<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>NaEun's Notes</title>
    <link>https://annovation.tistory.com/</link>
    <description> 급할수록도라에몽✨</description>
    <language>ko</language>
    <pubDate>Sat, 15 Aug 2026 10:26:09 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>annovation</managingEditor>
    <image>
      <title>NaEun's Notes</title>
      <url>https://tistory1.daumcdn.net/tistory/7468106/attach/98f5e792132c457486542605268da58a</url>
      <link>https://annovation.tistory.com</link>
    </image>
    <item>
      <title>[트러블슈팅] 재고 API - (3) 동시성 처리 최적화 : k6 자동 측정 스크립트의 jq 문법 오류 해결</title>
      <link>https://annovation.tistory.com/578</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;k6 자동 측정 스크립트의 jq 문법 오류 해결&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;부하 테스트는 완료됐지만 결과 계산 단계에서 오류가 발생했다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;k6를 이용한 재고 차감 부하 테스트는 정상적으로 완료됐지만, Micrometer 스냅샷과 k6 결과를 조합해 최종 판정을 생성하는 단계에서 jq 문법 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;jq: error: syntax error, unexpected if
jq: error: May need parentheses around object key expression
jq: 3 compile errors&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부하 요청과 측정값 수집은 이미 끝난 상태였고, JSON 결과를 계산하는 jq 코드만 실행되지 않은 상황이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;문제가 발생한 코드&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;객체의 값으로 사용한 if 표현식을 괄호로 감싸지 않았다&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;| .average = {
    lockWaitMs:
        if .delta.waitAcquired &amp;gt; 0 then
            (
                ($B.wait.acquired.totalTime
                - $A.wait.acquired.totalTime)
                / .delta.waitAcquired
            ) * 1000
        else
            null
        end,
    lockHoldMs:
        if .delta.holdSuccess &amp;gt; 0 then
            (
                ($B.hold.success.totalTime
                - $A.hold.success.totalTime)
                / .delta.holdSuccess
            ) * 1000
        else
            null
        end
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;jq 객체는 다음과 같이 키와 값으로 구성된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;{
    key: value
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순한 숫자나 필드 조회는 바로 값으로 사용할 수 있지만, &lt;code&gt;if-then-else-end&lt;/code&gt;와 같이 여러 구문으로 구성된 복합 표현식은 객체의 값이라는 범위를 명확하게 나타내기 위해 괄호로 감싸야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 문법 오류
{ value: if true then 1 else 0 end }

# 정상
{ value: (if true then 1 else 0 end) }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;괄호가 없으면 jq 파서는 &lt;code&gt;value:&lt;/code&gt; 뒤에 나온 &lt;code&gt;if&lt;/code&gt;를 객체의 값으로 정상 해석하지 못하고 &lt;code&gt;unexpected if&lt;/code&gt; 오류를 발생시킨다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;수정한 코드&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;if 표현식 전체를 괄호로 감쌌다&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;| .average = {
    lockWaitMs:
        (if .delta.waitAcquired &amp;gt; 0 then
            (
                ($B.wait.acquired.totalTime
                - $A.wait.acquired.totalTime)
                / .delta.waitAcquired
            ) * 1000
        else
            null
        end),
    lockHoldMs:
        (if .delta.holdSuccess &amp;gt; 0 then
            (
                ($B.hold.success.totalTime
                - $A.hold.success.totalTime)
                / .delta.holdSuccess
            ) * 1000
        else
            null
        end)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;수정 전후의 계산 방식은 동일하다. 괄호는 계산 결과를 바꾸는 것이 아니라, &lt;code&gt;if&lt;/code&gt; 표현식 전체가 객체 필드의 값이라는 것을 jq 파서에 명확하게 알려준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;코드의 동작 원리&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;스냅샷 A와 B의 차이를 이용해 측정 구간의 평균 시간을 계산한다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Micrometer Timer의 &lt;code&gt;COUNT&lt;/code&gt;와 &lt;code&gt;TOTAL_TIME&lt;/code&gt;은 애플리케이션 시작 이후 누적된다. 따라서 본 측정 직전의 스냅샷 A와 측정 직후의 스냅샷 B를 저장하고 두 값의 차이를 계산한다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;측정 구간 요청 수
= B.count - A.count

측정 구간 누적 시간
= B.totalTime - A.totalTime

평균 시간
= 측정 구간 누적 시간 / 측정 구간 요청 수&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Timer의 시간 단위는 초이므로 마지막에 &lt;code&gt;1000&lt;/code&gt;을 곱해 밀리초로 변환한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;평균 락 대기시간(ms)
= (대기 TOTAL_TIME 차이 / 락 획득 COUNT 차이) &amp;times; 1000

평균 락 점유시간(ms)
= (점유 TOTAL_TIME 차이 / 성공 COUNT 차이) &amp;times; 1000&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청 수가 0인 경우에는 0으로 나누는 오류를 방지하기 위해 평균값을 &lt;code&gt;null&lt;/code&gt;로 기록한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;검증 결과&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;기존 측정 파일을 이용해 부하 테스트를 다시 실행하지 않고 결과를 복구했다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류가 발생하기 전에 k6 결과와 Micrometer 스냅샷 A&amp;middot;B가 각각 JSON 파일로 저장되어 있었다.&lt;/li&gt;
&lt;li&gt;이번 오류는 측정 단계가 아니라 저장된 원본을 가공하는 최종 jq 계산 단계에서 발생했다.&lt;/li&gt;
&lt;li&gt;따라서 부하 테스트를 다시 실행하지 않고, jq 문법을 수정한 뒤 기존 원본 파일을 입력으로 결과 계산 단계만 다시 실행해 verdict를 생성했다.&lt;/li&gt;
&lt;li&gt;현재 자동화 스크립트가 중간 단계부터 자동 재개하는 것은 아니며, 복구 과정에서는 수정된 jq 계산 명령만 별도로 실행했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;최종 판정: VALID

요청 수: 13,668건
성공 수: 13,668건
락 타임아웃: 0건
예상하지 못한 실패: 0건
락 소유권 상실: 0건

성공 TPS: 227.8
평균 API 응답시간: 883.75ms
API p95: 1,020.76ms
API p99: 1,191.37ms

평균 락 대기시간: 878.32ms
평균 락 점유시간: 4.09ms&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;재고 감소량, k6 성공 요청 수, 락 획득 수, 락 점유 완료 수가 모두 13,668건으로 일치해 동시성 정합성 검사도 통과했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;배운 점&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;셸 문법 검사만으로는 내부 jq 문법까지 검증할 수 없다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;bash -n&lt;/code&gt;은 셸 스크립트 문법만 검사하며 문자열 안에 작성된 jq 프로그램의 문법은 검사하지 않는다.&lt;/li&gt;
&lt;li&gt;jq 코드가 포함된 자동화 스크립트는 실제 JSON 파일을 입력해 jq 프로그램까지 실행해봐야 한다.&lt;/li&gt;
&lt;li&gt;측정 원본과 최종 가공 결과를 분리해 저장하면 결과 계산이 실패하더라도 부하 테스트를 다시 실행하지 않고 복구할 수 있다.&lt;/li&gt;
&lt;li&gt;자동화 스크립트도 애플리케이션 코드처럼 정상 경로뿐 아니라 파싱 실패와 결과 파일 누락 같은 실패 경로를 검증해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고자료&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://jqlang.org/manual/v1.6/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jq 1.6 공식 매뉴얼 - Object Construction, if-then-else-end&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jqlang.org/tutorial/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jq 공식 튜토리얼&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/578</guid>
      <comments>https://annovation.tistory.com/578#entry578comment</comments>
      <pubDate>Tue, 4 Aug 2026 23:59:02 +0900</pubDate>
    </item>
    <item>
      <title>[리팩토링] 재고 API - (3) 동시성 처리 최적화 : Micrometer 도입</title>
      <link>https://annovation.tistory.com/576</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;코드 구현&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;기존 코드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785477468566&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class RedissonStockLockManager implements StockLockManager {

	private static final int MAX_RETRY_COUNT = 3;
	private static final long WAIT_TIME_SECONDS = 3L;
	private static final long LEASE_TIME_SECONDS = 5L;
	private static final long RETRY_BACKOFF_MILLIS = 100L;

	private final RedissonClient redissonClient;

	@Override
	public &amp;lt;T&amp;gt; T executeWithLock(String lockKey, Supplier&amp;lt;T&amp;gt; supplier) {
		RLock lock = redissonClient.getLock(lockKey);
		boolean locked = false;

		try {
			for (int retryCount = 0; retryCount &amp;lt; MAX_RETRY_COUNT; retryCount++) {
				locked = lock.tryLock(WAIT_TIME_SECONDS, LEASE_TIME_SECONDS, TimeUnit.SECONDS);
				if (locked) {
					return supplier.get();
				}

				Thread.sleep(RETRY_BACKOFF_MILLIS);
			}

			throw new GlobalException(STOCK_LOCK_TIMEOUT);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new GlobalException(STOCK_LOCK_TIMEOUT);
		} finally {
			if (locked &amp;amp;&amp;amp; lock.isHeldByCurrentThread()) {
				lock.unlock();
			}
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Timer 추가한 코드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785477488120&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class RedissonStockLockManager implements StockLockManager {

	private static final int MAX_RETRY_COUNT = 3;
	private static final long WAIT_TIME_SECONDS = 3L;
	private static final long LEASE_TIME_SECONDS = 5L;
	private static final long RETRY_BACKOFF_MILLIS = 100L;

	private final RedissonClient redissonClient;
	private final MeterRegistry meterRegistry;

	private final Timer waitAcquiredTimer;
	private final Timer waitTimeoutTimer;
	private final Timer waitInterruptedTimer;
	private final Timer waitErrorTimer;

	private final Timer holdSuccessTimer;
	private final Timer holdErrorTimer;
	private final Timer holdOwnershipLostTimer;

	public RedissonStockLockManager(RedissonClient redissonClient, MeterRegistry meterRegistry) {
		this.redissonClient = redissonClient;
		this.meterRegistry = meterRegistry;

		this.waitAcquiredTimer = createTimer(&quot;stock.lock.wait&quot;, &quot;acquired&quot;);
		this.waitTimeoutTimer = createTimer(&quot;stock.lock.wait&quot;, &quot;timeout&quot;);
		this.waitInterruptedTimer = createTimer(&quot;stock.lock.wait&quot;, &quot;interrupted&quot;);
		this.waitErrorTimer = createTimer(&quot;stock.lock.wait&quot;, &quot;error&quot;);

		this.holdSuccessTimer = createTimer(&quot;stock.lock.hold&quot;, &quot;success&quot;);
		this.holdErrorTimer = createTimer(&quot;stock.lock.hold&quot;, &quot;error&quot;);
		this.holdOwnershipLostTimer = createTimer(&quot;stock.lock.hold&quot;, &quot;ownership_lost&quot;);
	}

	@Override
	public &amp;lt;T&amp;gt; T executeWithLock(String lockKey, Supplier&amp;lt;T&amp;gt; supplier) {
		RLock lock = redissonClient.getLock(lockKey);
		Timer.Sample waitSample = Timer.start(meterRegistry);
		boolean waitRecorded = false;

		try {
			for (int retryCount = 0; retryCount &amp;lt; MAX_RETRY_COUNT; retryCount++) {
				boolean locked = lock.tryLock(WAIT_TIME_SECONDS, LEASE_TIME_SECONDS, TimeUnit.SECONDS);

				if (locked) {
					waitSample.stop(waitAcquiredTimer);
					waitRecorded = true;

					return executeWhileHoldingLock(lock, supplier);
				}

				Thread.sleep(RETRY_BACKOFF_MILLIS);
			}

			waitSample.stop(waitTimeoutTimer);
			waitRecorded = true;

			throw new GlobalException(STOCK_LOCK_TIMEOUT);
		} catch (InterruptedException e) {
			waitSample.stop(waitInterruptedTimer);
			waitRecorded = true;

			Thread.currentThread().interrupt();
			throw new GlobalException(STOCK_LOCK_TIMEOUT);
		} catch (RuntimeException e) {
			if (!waitRecorded) {
				waitSample.stop(waitErrorTimer);
			}

			throw e;
		}
	}

	private &amp;lt;T&amp;gt; T executeWhileHoldingLock(RLock lock, Supplier&amp;lt;T&amp;gt; supplier) {
		Timer.Sample holdSample = Timer.start(meterRegistry);
		boolean supplierSucceeded = false;
		boolean unlockSucceeded = false;
		boolean ownershipLost = false;

		try {
			T result = supplier.get();
			supplierSucceeded = true;
			return result;
		} finally {
			try {
				// 다른 요청이 임계 구역에 진입했을 수 있는 락 소유권 상실을 별도로 기록한다.
				if (lock.isHeldByCurrentThread()) {
					lock.unlock();
					unlockSucceeded = true;
				} else {
					ownershipLost = true;
				}
			} finally {
				holdSample.stop(resolveHoldTimer(ownershipLost, supplierSucceeded, unlockSucceeded));
			}
		}
	}

	private Timer resolveHoldTimer(
			boolean ownershipLost, boolean supplierSucceeded, boolean unlockSucceeded) {
		if (ownershipLost) {
			return holdOwnershipLostTimer;
		}

		if (!supplierSucceeded || !unlockSucceeded) {
			return holdErrorTimer;
		}

		return holdSuccessTimer;
	}

	private Timer createTimer(String name, String result) {
		return Timer.builder(name).tag(&quot;result&quot;, result).register(meterRegistry);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;코드 구조&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;핵심 로직&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785477546413&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;executeWithLock()
&amp;rarr; 락 획득을 책임진다.

supplier.get()
&amp;rarr; 락 안에서 실행할 실제 비즈니스 작업이다.

executeWhileHoldingLock()
&amp;rarr; 작업 실행, 락 해제, 보유 시간 기록을 책임진다.

wait Timer
&amp;rarr; 락을 얻기 전까지의 시간을 측정한다.

hold Timer
&amp;rarr; 락을 얻은 후 해제할 때까지의 시간을 측정한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;전체 흐름&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785477581210&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;executeWithLock()
│
├─ Redis 락 객체 가져오기
│
├─ wait Timer 시작
│
├─ 최대 3번 tryLock()
│   │
│   ├─ 성공
│   │   ├─ wait acquired 기록
│   │   └─ executeWhileHoldingLock()
│   │       ├─ hold Timer 시작
│   │       ├─ supplier.get()
│   │       ├─ 락 해제
│   │       └─ success/error/ownership_lost 기록
│   │
│   └─ 모두 실패
│       ├─ wait timeout 기록
│       └─ 예외 발생
│
├─ 인터럽트
│   ├─ wait interrupted 기록
│   └─ 예외 발생
│
└─ 기타 오류
    ├─ wait error 기록
    └─ 원래 예외 다시 발생&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Timer를 추가한 이유&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드에서도 분산 락을 이용한 재고 정합성은 보장할 수 있었다. 하지만 API 응답시간이 느려졌을 때 실제로 어느 구간에서 시간이 오래 걸렸는지는 확인할 수 없었다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;전체 재고 차감 응답시간
=
락 획득 대기시간
+ 락 내부 작업시간
+ 기타 요청 처리시간&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 전체 응답시간만 제공하는 k6 결과와 함께 애플리케이션 내부의 락 대기시간과 점유시간을 확인하기 위해 Micrometer Timer를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;변경 전과 변경 후&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; text-align: center;&quot;&gt;&lt;b&gt;변경 전&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; text-align: center;&quot;&gt;&lt;b&gt;변경 후&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;락 대기시간&lt;/td&gt;
&lt;td&gt;측정할 수 없음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stock.lock.wait&lt;/code&gt;로 측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;락 점유시간&lt;/td&gt;
&lt;td&gt;측정할 수 없음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stock.lock.hold&lt;/code&gt;로 측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;실패 원인&lt;/td&gt;
&lt;td&gt;예외 결과로만 확인&lt;/td&gt;
&lt;td&gt;Timer의 &lt;code&gt;result&lt;/code&gt; 태그로 분류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;락 소유권 상실&lt;/td&gt;
&lt;td&gt;별도 기록 없음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ownership_lost&lt;/code&gt;로 별도 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Timer 측정 구간&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;락 대기시간&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stock.lock.wait

Timer 시작
&amp;rarr; tryLock() 및 재시도
&amp;rarr; 락 획득 성공 또는 실패 결과 확정
&amp;rarr; Timer 종료&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30%; text-align: center;&quot;&gt;&lt;b&gt;result 태그&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;acquired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;락 획득 성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;설정된 재시도 횟수를 모두 소진&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;interrupted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;락 대기 중 스레드 인터럽트 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Redis 호출 등 예상하지 못한 오류 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;락 점유시간&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stock.lock.hold

락 획득 성공
&amp;rarr; Timer 시작
&amp;rarr; supplier.get() 실행
&amp;rarr; 락 소유권 확인
&amp;rarr; unlock() 완료
&amp;rarr; Timer 종료&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30%; text-align: center;&quot;&gt;&lt;b&gt;result 태그&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;success&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;비즈니스 작업과 락 해제 모두 성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;비즈니스 작업 또는 락 해제 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;ownership_lost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;락 해제 전에 현재 스레드가 락 소유권을 잃은 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;중복 기록 방지&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;executeWithLock()&lt;/code&gt; 외부의 &lt;code&gt;catch (RuntimeException e)&lt;/code&gt;는 락 획득 과정뿐 아니라 &lt;code&gt;supplier.get()&lt;/code&gt;이나 &lt;code&gt;unlock()&lt;/code&gt;에서 발생한 예외도 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 락을 이미 획득해 &lt;code&gt;wait acquired&lt;/code&gt;를 기록한 후 비즈니스 로직에서 예외가 발생했을 때, 동일한 대기시간이 &lt;code&gt;wait error&lt;/code&gt;로 다시 기록되지 않도록 &lt;code&gt;waitRecorded&lt;/code&gt; 값을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (locked) {
	waitSample.stop(waitAcquiredTimer);
	waitRecorded = true;

	return executeWhileHoldingLock(lock, supplier);
}

catch (RuntimeException e) {
	if (!waitRecorded) {
		waitSample.stop(waitErrorTimer);
	}

	throw e;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;waitRecorded = false
&amp;rarr; 아직 락 대기 결과를 기록하지 않음
&amp;rarr; RuntimeException 발생 시 wait error 기록

waitRecorded = true
&amp;rarr; acquired 또는 timeout 결과를 이미 기록함
&amp;rarr; wait error를 중복 기록하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Actuator를 통한 측정값 확인&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loadtest 프로필에서는 Actuator의 metrics 엔드포인트를 통해 Timer 측정값을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl 'http://localhost:8085/actuator/metrics/stock.lock.wait?tag=result:acquired'

curl 'http://localhost:8085/actuator/metrics/stock.lock.hold?tag=result:success'&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30%; text-align: center;&quot;&gt;&lt;b&gt;측정값&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;COUNT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Timer에 기록된 요청 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;TOTAL_TIME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기록된 전체 누적 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;code&gt;MAX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;참고용 최대 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;본 측정 구간의 평균 계산&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Micrometer Timer는 애플리케이션 시작 이후의 값을 누적한다. 따라서 warmup 데이터가 본 측정 결과에 포함되지 않도록 warmup 종료 후 지표 A와 compare 종료 후 지표 B를 저장한다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;본 측정 요청 수
= B.COUNT - A.COUNT

본 측정 누적 시간
= B.TOTAL_TIME - A.TOTAL_TIME

본 측정 평균 시간
= (B.TOTAL_TIME - A.TOTAL_TIME)
  / (B.COUNT - A.COUNT)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 락 시간은 위 방식으로 평균을 계산하고, 전체 API의 평균, p95, p99 및 성공 TPS는 k6 결과를 사용한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;검증&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL이나 Redis에 직접 연결하지 않는 단위 테스트를 작성하고 Mockito와 &lt;code&gt;SimpleMeterRegistry&lt;/code&gt;를 사용해 다음 경로를 검증했다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 락 즉시 획득
2. 재시도 후 락 획득
3. 재시도 소진 후 타임아웃
4. 락 대기 중 인터럽트
5. tryLock() 실행 중 RuntimeException
6. supplier 실행 중 RuntimeException
7. unlock() 실행 중 RuntimeException
8. 락 소유권 상실&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 loadtest 프로필로 애플리케이션을 실행하고 재고 차감 API를 호출하여 다음 결과를 확인했다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;wait acquired COUNT: 0 &amp;rarr; 1
hold success COUNT: 0 &amp;rarr; 1

wait timeout: 0
wait interrupted: 0
wait error: 0
hold error: 0
hold ownership_lost: 0&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;정리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 변경에서는 기존 분산 락의 재시도, 타임아웃, 예외 처리 정책을 변경하지 않고 관측을 위한 Timer만 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 k6에서 확인한 전체 API 응답시간과 애플리케이션 내부의 락 대기시간 및 점유시간을 함께 비교할 수 있게 되었고, 이후 락 점유 구간 리팩토링의 필요성과 개선 결과를 수치로 검증할 수 있는 기반을 마련했다.&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/576</guid>
      <comments>https://annovation.tistory.com/576#entry576comment</comments>
      <pubDate>Sun, 2 Aug 2026 23:59:03 +0900</pubDate>
    </item>
    <item>
      <title>[리팩토링] 재고 API - (3) 동시성 처리 최적화 : Micrometer 란?</title>
      <link>https://annovation.tistory.com/573</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Micrometer란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Micrometer란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Micrometer&lt;/b&gt;는 Java 애플리케이션의 메트릭을 수집하기 위한 계측 라이브러리이다.&lt;/li&gt;
&lt;li&gt;메트릭을 직접 장기간 저장하거나 대시보드로 시각화하지는 않는다.&lt;/li&gt;
&lt;li&gt;Prometheus, Datadog 등 다양한 모니터링 시스템에 연결할 수 있는 공통 API를 제공한다.&lt;/li&gt;
&lt;li&gt;동일한 계측 코드를 유지하면서 모니터링 시스템을 변경할 수 있도록 설계되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메트릭(Metric)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 상태를 숫자로 표현한 측정 데이터이다.&lt;/li&gt;
&lt;li&gt;예: HTTP 요청 수, HTTP 응답 시간, JVM Heap 사용량, DB Connection 수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Micrometer가 해결하는 문제&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus와 Datadog 같은 모니터링 시스템은 서로 다른 계측 방식을 사용한다.&lt;/li&gt;
&lt;li&gt;Micrometer는 공통 API를 제공하여 애플리케이션 코드의 변경을 줄인다.&lt;/li&gt;
&lt;li&gt;이를 통해 특정 모니터링 시스템에 강하게 종속되는 문제인 &lt;b&gt;Vendor Lock-in&lt;/b&gt;을 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;box-sizing: border-box; margin: 20px 0; padding: 16px; border-left: 5px solid #4aa3df; background-color: #f4f9fc;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할을 구분하면&lt;/b&gt;&lt;br /&gt;Micrometer는 애플리케이션 안에서 값을 측정하고, Prometheus는 그 값을 주기적으로 수집&amp;middot;저장하며, Grafana는 저장된 값을 그래프로 보여준다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;공식 문서&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문서명&lt;/b&gt;: Micrometer Documentation&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위치&lt;/b&gt;: Introduction&lt;/li&gt;
&lt;li&gt;&lt;b&gt;근거&lt;/b&gt;: Micrometer는 여러 관측 시스템의 계측 클라이언트 위에 공통 facade를 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크&lt;/b&gt;: &lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Documentation 열기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Meter란?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Meter&lt;/b&gt;는 Micrometer에서 메트릭을 측정하는 기본 단위이다. 무엇을 측정할 것인지에 따라 서로 다른 Meter를 사용하며, 모든 Meter는 MeterRegistry에 등록되어 관리된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;Meter&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;측정하는 값&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;대표 사용 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Counter&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;누적 횟수&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;회원가입 수, 예외 발생 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Timer&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;실행 시간과 실행 횟수&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;HTTP 요청, DB 조회 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Gauge&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;현재 상태 값&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;메모리 사용량, Connection Pool 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;DistributionSummary&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;값의 크기와 분포&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;파일 크기, 응답 크기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;LongTaskTimer&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;진행 중인 긴 작업의 시간&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;배치, 대용량 데이터 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;FunctionCounter&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;함수가 반환하는 누적값&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;기존 객체의 누적값 노출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;FunctionTimer&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;함수가 반환하는 횟수와 총 시간&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;기존 객체의 count와 time 노출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Meter는 &amp;ldquo;&lt;b&gt;무엇을 측정할 것인가&lt;/b&gt;&amp;rdquo;를 나타내는 추상적인 개념이고, Counter와 Timer는 Meter의 구체적인 종류이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;공식 문서&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문서명&lt;/b&gt;: Micrometer Meters&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위치&lt;/b&gt;: Concepts &amp;rarr; Meters&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크&lt;/b&gt;: &lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/meters.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Meters 열기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Timer란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Timer란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Timer&lt;/b&gt;는 Micrometer가 제공하는 Meter 중 하나이다.&lt;/li&gt;
&lt;li&gt;짧게 실행되는 작업의 &lt;b&gt;실행 시간(Latency)&lt;/b&gt;과 &lt;b&gt;실행 횟수(Frequency)&lt;/b&gt;를 함께 측정한다.&lt;/li&gt;
&lt;li&gt;한 번 시간을 재고 끝나는 스톱워치가 아니라, 같은 작업이 여러 번 실행될 때 측정 결과를 계속 누적하는 Meter이다.&lt;/li&gt;
&lt;li&gt;HTTP 요청 처리, DB 조회, Redis 조회, 외부 API 호출, 락 대기 시간 등에 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Latency(지연 시간)&lt;/b&gt;는 작업이 시작된 순간부터 종료될 때까지 걸린 시간이다. 요청 처리에 120ms가 걸렸다면 해당 요청의 latency는 120ms이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Frequency(발생 빈도)&lt;/b&gt;는 동일한 작업이 몇 번 수행되었는지를 뜻한다. 상품 조회가 1분 동안 500번 실행되었다면 해당 구간의 실행 횟수는 500회이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;공식 문서&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문서명&lt;/b&gt;: Micrometer Timers&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위치&lt;/b&gt;: Concepts &amp;rarr; Timers&lt;/li&gt;
&lt;li&gt;&lt;b&gt;근거&lt;/b&gt;: Timer는 짧은 작업의 latency와 발생 빈도를 측정하기 위한 Meter이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크&lt;/b&gt;: &lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/timers.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Timers 열기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Timer가 기록하는 값&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;값&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;count&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;측정이 완료된 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;totalTime&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;모든 실행 시간을 더한 누적 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;max&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;관찰 시간 창 안에서 가장 오래 걸린 실행 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Histogram&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;실행 시간을 구간(bucket)별 누적 개수로 기록한 데이터(설정 시)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Percentile&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;P50, P95, P99 같은 백분위 값(설정 시)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 시간이 100ms, 200ms, 300ms였다면 다음과 같이 누적된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;count&lt;/b&gt; = 3&lt;/li&gt;
&lt;li&gt;&lt;b&gt;totalTime&lt;/b&gt; = 600ms&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mean&lt;/b&gt; = 600ms &amp;divide; 3 = 200ms&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max&lt;/b&gt; = 300ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Histogram과 Percentile은 Timer를 등록했다고 항상 자동으로 생성되는 값이 아니다. 분포 통계 설정을 활성화해야 하며, 실제 노출 방식은 사용하는 MeterRegistry 구현체에 따라 달라질 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;왜 Timer를 사용하는가?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 기능 성공 여부와 성능은 서로 다른 문제이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 요청이 모두 성공하더라도 하나는 80ms, 다른 하나는 2,300ms가 걸릴 수 있다. 사용자는 성공 여부뿐 아니라 기다린 시간도 체감하므로 실행 시간을 별도로 측정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 로그와 메트릭은 목적이 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;long start = System.currentTimeMillis();
productService.findById(id);
long elapsed = System.currentTimeMillis() - start;
log.info(&quot;elapsed={}ms&quot;, elapsed);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 특정 요청의 상세 상황을 확인할 때 유용하다. 그러나 전체 요청의 평균, P95, 최대값과 시간에 따른 변화를 보려면 로그를 다시 집계해야 한다. Timer는 반복 실행 결과를 메트릭으로 누적해 이러한 집계를 쉽게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 시간을 잴 수 있는 작업은 Counter를 중복 생성할 필요가 없는 경우가 많다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Counter는 횟수만 측정하지만 Timer는 시간과 횟수를 함께 기록한다. 단, timeout처럼 별도의 사건 자체를 명확하게 세어야 한다면 별도 Counter가 유용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 평균만 보면 일부 매우 느린 요청이 가려질 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;99개가 100ms이고 1개가 10초라면 평균은 약 199ms이다. 평균만 보면 한 사용자가 10초를 기다렸다는 사실이 잘 드러나지 않으므로 P95, P99, max 같은 지표를 함께 본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Timer 등록과 MeterRegistry&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Timer.builder()로 등록하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Timer를 사용하려면 먼저 MeterRegistry에 등록해야 한다. Builder는 Timer의 이름, 설명, 태그, 분포 통계 설정을 구성하고, &lt;code&gt;register(meterRegistry)&lt;/code&gt;가 실제 Timer를 등록하고 반환한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer lockWaitTimer = Timer.builder(&quot;stock.lock.wait&quot;)
    .description(&quot;재고 락 획득 대기 시간&quot;)
    .tag(&quot;lock.type&quot;, &quot;redisson&quot;)
    .publishPercentiles(0.5, 0.95, 0.99)
    .minimumExpectedValue(Duration.ofMillis(1))
    .maximumExpectedValue(Duration.ofSeconds(3))
    .register(meterRegistry);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정의 의미는 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;설정&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;stock.lock.wait&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;메트릭 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;description()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;메트릭의 목적을 설명한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;tag()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;값의 종류가 제한적인 분류 정보를 추가한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;publishPercentiles()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;이 애플리케이션 인스턴스에서 P50, P95, P99를 계산한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;minimumExpectedValue()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;예상 최소 시간을 분포 통계 설정에 제공한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;maximumExpectedValue()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;예상 최대 시간을 분포 통계 설정에 제공한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;register()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;설정된 Timer를 MeterRegistry에 등록하고 반환한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;box-sizing: border-box; margin: 20px 0; padding: 16px; border-left: 5px solid #e0a12b; background-color: #fff9e8;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시 이름 주의&lt;/b&gt;&lt;br /&gt;&lt;code&gt;http.server.requests&lt;/code&gt;는 Spring Boot가 자동으로 제공하는 HTTP 서버 메트릭 이름과 충돌하거나 혼동될 수 있다. 직접 만든 락 메트릭에는 &lt;code&gt;stock.lock.wait&lt;/code&gt;처럼 목적이 분명한 별도 이름을 사용하는 편이 안전하다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus에서 여러 인스턴스의 분포를 합칠 계획이라면 다음처럼 histogram을 등록할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer lockWaitTimer = Timer.builder(&quot;stock.lock.wait&quot;)
    .description(&quot;재고 락 획득 대기 시간&quot;)
    .publishPercentileHistogram()
    .serviceLevelObjectives(
        Duration.ofMillis(10),
        Duration.ofMillis(50),
        Duration.ofMillis(100),
        Duration.ofMillis(300),
        Duration.ofSeconds(1)
    )
    .minimumExpectedValue(Duration.ofMillis(1))
    .maximumExpectedValue(Duration.ofSeconds(3))
    .register(meterRegistry);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;serviceLevelObjectives()&lt;/code&gt;는 &amp;ldquo;100ms 이하 요청 비율&amp;rdquo;처럼 팀이 중요하게 보는 경계의 bucket을 추가한다. 이는 percentile 자체를 지정하는 설정이 아니라 특정 시간 기준을 직접 관찰하기 위한 설정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;왜 register()가 필요한가?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Builder는 아직 Timer의 설정 정보를 구성하는 단계이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;register()&lt;/code&gt;를 호출해야 MeterRegistry에서 관리되는 실제 Timer를 얻는다.&lt;/li&gt;
&lt;li&gt;같은 이름과 같은 태그 조합으로 다시 등록하면 Registry는 일반적으로 이미 등록된 Meter를 반환한다.&lt;/li&gt;
&lt;li&gt;이름은 같아도 태그 값이 다르면 별도의 Meter ID로 관리된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer getTimer = Timer.builder(&quot;product.lookup&quot;)
    .tag(&quot;method&quot;, &quot;GET&quot;)
    .register(meterRegistry);

Timer postTimer = Timer.builder(&quot;product.lookup&quot;)
    .tag(&quot;method&quot;, &quot;POST&quot;)
    .register(meterRegistry);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 Timer는 이름은 같지만 태그가 다르므로 서로 다른 Meter이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;MeterRegistry란?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MeterRegistry&lt;/b&gt;는 Meter를 등록하고 이름과 태그 조합으로 관리하며, Registry 구현체에 맞는 형태로 측정값을 제공하는 중심 컴포넌트이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Counter, Timer, Gauge 같은 Meter를 생성&amp;middot;등록&amp;middot;조회한다.&lt;/li&gt;
&lt;li&gt;같은 Meter ID가 불필요하게 중복 생성되지 않도록 관리한다.&lt;/li&gt;
&lt;li&gt;각 Meter가 기록한 현재 누적 상태를 애플리케이션 메모리에서 관리한다.&lt;/li&gt;
&lt;li&gt;Prometheus Registry라면 scrape 가능한 형식으로 값을 노출할 수 있게 연결한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;MeterRegistry가 매 요청마다 Prometheus에 메트릭을 전송한다&amp;rdquo;라고 이해하면 안 된다. 일반적인 Prometheus 구성에서는 요청 처리 중 Timer가 메모리의 값을 갱신하고, Prometheus가 별도의 주기로 메트릭 엔드포인트를 조회한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;공식 문서&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문서명&lt;/b&gt;: Micrometer Registry&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위치&lt;/b&gt;: Concepts &amp;rarr; Registry&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크&lt;/b&gt;: &lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/registry.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Registry 열기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Percentile과 Histogram&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Percentile이란?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Percentile(백분위수)&lt;/b&gt;은 여러 측정값을 작은 값부터 정렬했을 때 전체 중 일정 비율이 어느 값 이하에 위치하는지 보여준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;P50 = 10ms&lt;/b&gt;: 요청의 약 50%가 10ms 이내에 완료되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;P95 = 100ms&lt;/b&gt;: 요청의 약 95%가 100ms 이내에 완료되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;P99 = 500ms&lt;/b&gt;: 요청의 약 99%가 500ms 이내에 완료되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P95가 100ms라는 말은 평균이 100ms라는 뜻이 아니다. 요청의 약 95%가 100ms 이내에 완료되었고 나머지 약 5%는 그보다 오래 걸렸다는 뜻이다.&lt;/p&gt;
&lt;div style=&quot;box-sizing: border-box; margin: 20px 0; padding: 16px; border-left: 5px solid #4aa3df; background-color: #f4f9fc;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 구분&lt;/b&gt;&lt;br /&gt;요청 한 건에는 P95가 없다. 요청 한 건에는 10ms, 200ms처럼 실행 시간 하나만 있다. P95는 여러 요청의 실행 시간을 모은 분포에서 계산한다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;publishPercentiles()&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;publishPercentiles()&lt;/code&gt;는 Micrometer가 애플리케이션 인스턴스 내부에서 지정된 percentile을 계산해 내보내도록 설정한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer.builder(&quot;stock.lock.wait&quot;)
    .publishPercentiles(0.5, 0.95, 0.99)
    .register(meterRegistry);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0.5는 P50, 0.95는 P95, 0.99는 P99를 의미한다.&lt;/li&gt;
&lt;li&gt;사용자별 P95를 구한 뒤 평균 내는 방식이 아니다.&lt;/li&gt;
&lt;li&gt;해당 인스턴스가 측정한 여러 요청의 분포에서 percentile을 계산한다.&lt;/li&gt;
&lt;li&gt;단일 Spring Boot 인스턴스의 로컬 성능 실험에서 결과를 바로 확인하기 편하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한계: 서버별 percentile은 전체 percentile로 다시 합칠 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;서버&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;요청 수&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;P95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;서버 A&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;10,000건&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;서버 B&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;10건&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;1,000ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 P95의 평균 550ms는 전체 10,010건의 P95가 아니다. Percentile은 이미 분포를 하나의 경계값으로 압축한 결과이므로 단순 합산이나 평균으로 원래 분포를 복원할 수 없다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Bucket과 publishPercentileHistogram()&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Bucket&lt;/b&gt;은 측정된 시간을 분류하는 시간 구간이다. Prometheus의 classic histogram bucket은 보통 누적형이므로 &amp;ldquo;100ms 이하&amp;rdquo; bucket에는 10ms, 50ms 이하 요청도 모두 포함된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;요청&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;락 대기 시간&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;A&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;8ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;B&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;40ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;C&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;80ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;D&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;250ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;E&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;700ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;누적 Bucket&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;포함 요청&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;개수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;10ms 이하&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;A&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;50ms 이하&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;A, B&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;100ms 이하&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;A, B, C&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;300ms 이하&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;A, B, C, D&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;1초 이하&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;A, B, C, D, E&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stock_lock_wait_seconds_bucket{le=&quot;0.01&quot;} 1
stock_lock_wait_seconds_bucket{le=&quot;0.05&quot;} 2
stock_lock_wait_seconds_bucket{le=&quot;0.1&quot;} 3
stock_lock_wait_seconds_bucket{le=&quot;0.3&quot;} 4
stock_lock_wait_seconds_bucket{le=&quot;1.0&quot;} 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;le&lt;/code&gt;는 less than or equal, 즉 &amp;ldquo;이 값 이하&amp;rdquo;라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;publishPercentileHistogram()&lt;/code&gt;은 이러한 bucket을 모니터링 시스템에 노출할 수 있게 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer.builder(&quot;stock.lock.wait&quot;)
    .publishPercentileHistogram()
    .register(meterRegistry);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 애플리케이션 인스턴스는 같은 경계의 bucket별 누적 개수를 제공한다.&lt;/li&gt;
&lt;li&gt;Prometheus는 같은 bucket끼리 합산할 수 있다.&lt;/li&gt;
&lt;li&gt;합산된 전체 분포를 기반으로 다중 인스턴스의 P95, P99 근삿값을 계산할 수 있다.&lt;/li&gt;
&lt;li&gt;Bucket 경계 사이의 실제 값은 알 수 없으므로 histogram percentile은 근삿값이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;publishPercentiles()&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;publishPercentileHistogram()&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;계산 주체&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;애플리케이션&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;Prometheus 같은 백엔드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;노출 데이터&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;계산된 P50, P95, P99&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;Bucket별 누적 개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;서버 간 집계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;전체 percentile로 재집계 불가&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;동일 bucket을 합산해 재집계 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;적합한 경우&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;단일 인스턴스 실험, 개별 인스턴스 확인&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;다중 인스턴스 운영 집계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 설정을 동시에 사용할 수도 있지만 분포 통계 메모리 비용과 노출 시계열 수가 늘 수 있다. 단일 인스턴스 포트폴리오 실험이라면 &lt;code&gt;publishPercentiles()&lt;/code&gt;만으로 시작하고, 다중 인스턴스 및 장기 시계열 운영이 필요해질 때 histogram과 Prometheus를 도입하는 선택도 충분히 타당하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;공식 문서&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문서명&lt;/b&gt;: Micrometer Histograms and Percentiles&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위치&lt;/b&gt;: Concepts &amp;rarr; Histograms and Percentiles&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크&lt;/b&gt;: &lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/histogram-quantiles.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Histograms and Percentiles 열기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Timer 시간 측정 방법&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Timer를 등록한 다음에는 실제 작업 시간을 기록해야 한다. 대표적인 방법은 &lt;code&gt;record()&lt;/code&gt;, &lt;code&gt;Timer.Sample&lt;/code&gt;, &lt;code&gt;@Timed&lt;/code&gt; 세 가지이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;방법&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;측정 범위&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;적합한 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;record()&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;하나의 코드 블록&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;Timer가 미리 정해져 있고 시작&amp;middot;종료가 붙어 있을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Timer.Sample&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;개발자가 정한 시작부터 종료까지&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;결과별 태그 또는 세부 구간 측정이 필요할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;@Timed&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;메서드 전체&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;메서드 경계를 간단히 자동 측정할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;1. record()&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;record()&lt;/code&gt;는 Timer가 코드 블록 실행 전후의 시간을 측정해 같은 Timer에 기록하는 가장 단순한 방법이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer databaseTimer = Timer.builder(&quot;stock.database.decrease&quot;)
    .publishPercentiles(0.5, 0.95, 0.99)
    .register(meterRegistry);

databaseTimer.record(() -&amp;gt; {
    stockRepository.decrease(productId, quantity);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값이 있는 작업은 &lt;code&gt;recordCallable()&lt;/code&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Product product = databaseTimer.recordCallable(() -&amp;gt;
    productRepository.findById(productId)
        .orElseThrow()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적인 동작 순서는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시작 시간을 확인한다.&lt;/li&gt;
&lt;li&gt;현재 호출 스레드에서 전달받은 작업을 실행한다.&lt;/li&gt;
&lt;li&gt;성공하거나 예외로 종료되면 경과 시간을 계산한다.&lt;/li&gt;
&lt;li&gt;Timer의 count와 totalTime 등에 결과를 기록한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; &lt;code&gt;record()&lt;/code&gt;는 작업을 비동기로 바꾸지 않는다. 내부 작업이 2초 걸리면 호출한 스레드도 그 작업이 끝날 때까지 실행을 계속하지 못한다. Timer가 추가하는 비용과 실제 측정 대상 작업의 실행 시간은 구분해야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;2. Timer.Sample&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Timer.Sample&lt;/b&gt;은 시작 시각을 보관했다가, 작업이 끝난 뒤 선택한 Timer에 경과 시간을 기록하는 측정용 객체이다. Sample 자체는 count나 totalTime을 누적하는 Timer가 아니다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer.Sample = 시작 시각을 들고 있는 스톱워치
Timer        = 측정 결과를 계속 누적하는 기록장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sample은 결과를 알기 전에 측정을 시작하고, 결과를 확인한 뒤 태그를 선택해야 할 때 특히 유용하다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer.Sample waitSample = Timer.start(meterRegistry);
String result = &quot;error&quot;;

try {
    boolean acquired = lock.tryLock(
        1,
        3,
        TimeUnit.SECONDS
    );

    result = acquired ? &quot;acquired&quot; : &quot;timeout&quot;;

    if (!acquired) {
        throw new StockLockTimeoutException();
    }
} catch (InterruptedException e) {
    result = &quot;interrupted&quot;;
    Thread.currentThread().interrupt();
    throw new IllegalStateException(&quot;락 획득 중 인터럽트 발생&quot;, e);
} finally {
    waitSample.stop(
        meterRegistry.timer(
            &quot;stock.lock.wait&quot;,
            &quot;result&quot;,
            result
        )
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 &lt;code&gt;tryLock()&lt;/code&gt; 직전부터 성공, 타임아웃 또는 인터럽트 결과가 정해질 때까지의 &lt;b&gt;락 대기 시간&lt;/b&gt;을 기록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 실제로 보유한 전체 시간은 락 획득 직후 Sample을 시작하고 &lt;code&gt;unlock()&lt;/code&gt; 완료 뒤 중지한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Timer.Sample holdSample = Timer.start(meterRegistry);

try {
    stockRepository.decrease(productId, quantity);
} finally {
    try {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    } finally {
        holdSample.stop(
            meterRegistry.timer(&quot;stock.lock.hold&quot;)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 측정 범위는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;락 획득 성공
&amp;rarr; 임계 구역의 재고 차감 작업
&amp;rarr; unlock() 실행 완료
&amp;rarr; stock.lock.hold에 기록&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sample을 시작하고 &lt;code&gt;stop()&lt;/code&gt;하지 않으면 아무 Timer에도 기록되지 않는다.&lt;/li&gt;
&lt;li&gt;예외 경로에서도 정확히 한 번 중지되도록 흐름을 설계해야 한다.&lt;/li&gt;
&lt;li&gt;동일한 Sample을 여러 번 중지하는 구조는 피해야 한다.&lt;/li&gt;
&lt;li&gt;결과 태그는 &lt;code&gt;acquired&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;interrupted&lt;/code&gt;처럼 종류가 제한된 값만 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;3. @Timed&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Timed&lt;/code&gt;는 애너테이션이 붙은 메서드의 실행 시간을 AOP로 측정한다. 개발자가 메서드 안에서 Timer의 시작과 종료 코드를 직접 작성하지 않아도 된다는 장점이 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Timed(
    value = &quot;stock.decrease.total&quot;,
    percentiles = {0.5, 0.95, 0.99}
)
public void decreaseStock(Long productId, int quantity) {
    // 락 획득, 재고 차감, 락 반환
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;호출자
  &amp;darr;
Spring AOP Proxy
  &amp;darr; 시작 시간 기록
실제 @Timed 메서드 실행
  &amp;darr; 종료 시간 기록
Timer에 경과 시간 누적&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Timed&lt;/code&gt;가 실제로 동작하려면 현재 프로젝트의 Spring Boot와 Micrometer 구성에 맞는 애너테이션 계측 설정이 필요하다. Micrometer의 &lt;code&gt;TimedAspect&lt;/code&gt;를 직접 사용할 경우에는 다음처럼 Bean으로 등록할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class MetricsConfig {

    @Bean
    public TimedAspect timedAspect(MeterRegistry meterRegistry) {
        return new TimedAspect(meterRegistry);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;box-sizing: border-box; margin: 20px 0; padding: 16px; border-left: 5px solid #d9534f; background-color: #fff4f4;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;버전과 구성 확인 필요&lt;/b&gt;&lt;br /&gt;애너테이션 계측 활성화 방식과 필요한 의존성은 Spring Boot 및 Micrometer 버전에 따라 달라질 수 있다. 실제 프로젝트에서는 사용 중인 버전의 공식 문서를 기준으로 확인해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Timed의 측정 범위&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;메서드 진입
&amp;rarr; 락 획득 대기
&amp;rarr; 락 획득
&amp;rarr; 재고 차감
&amp;rarr; 락 반환
&amp;rarr; 메서드 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;@Timed&lt;/code&gt; 하나만 사용하면 전체 작업이 느리다는 사실은 알 수 있지만, 락 대기와 락 점유 중 어느 구간이 병목인지는 분리하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Self-invocation 주의&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StockService {

    public void processOrder() {
        decreaseStock(); // 같은 객체 내부 호출
    }

    @Timed(&quot;stock.decrease.total&quot;)
    public void decreaseStock() {
        // 재고 차감
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 객체 안에서 직접 호출하면 Spring AOP Proxy를 거치지 않으므로 &lt;code&gt;@Timed&lt;/code&gt;가 적용되지 않을 수 있다. 다른 Spring Bean이 프록시를 통해 호출하는 구조인지 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 작업 주의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드가 &lt;code&gt;CompletableFuture&lt;/code&gt;, Reactor &lt;code&gt;Mono&lt;/code&gt;&amp;middot;&lt;code&gt;Flux&lt;/code&gt; 등을 실제 완료 전에 반환한다면 단순 메서드 경계 측정과 실제 비동기 완료 시간은 다를 수 있다. 비동기 작업은 사용하는 계측 방식이 완료 신호까지 추적하는지 별도로 확인해야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;record(), Timer.Sample, @Timed 비교&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;record()&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;Timer.Sample&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;@Timed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;범위&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;코드 블록&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;직접 지정&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;메서드 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Timer 선택&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;시작 전&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;종료 시 선택 가능&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;애너테이션 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;동적 결과 태그&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;직접 분기 필요&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;적합&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;세밀한 제어는 제한적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;AOP 필요&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;아니오&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;아니오&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;Self-invocation 영향&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;b&gt;대표 사례&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;DB 호출 한 구간&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;락 대기&amp;middot;점유 시간&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;서비스 메서드 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;동시성 처리 구간 측정&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 성능을 분석하려면 &lt;b&gt;락 대기 시간&lt;/b&gt;과 &lt;b&gt;락 점유 시간&lt;/b&gt;을 구분해야 한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;메트릭&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;측정 범위&lt;/th&gt;
&lt;th style=&quot;padding: 8px;&quot;&gt;권장 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;stock.decrease.total&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;메서드 진입부터 종료까지&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;@Timed&lt;/code&gt; 또는 기존 HTTP 자동 계측&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;stock.lock.wait&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;락 획득 시도부터 결과 반환까지&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;Timer.Sample&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;stock.lock.hold&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;락 획득부터 unlock 완료까지&lt;/td&gt;
&lt;td style=&quot;padding: 8px;&quot;&gt;&lt;code&gt;Timer.Sample&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;락 획득 시도
  &amp;darr; stock.lock.wait 시작
다른 요청의 락 반환까지 대기
  &amp;darr; stock.lock.wait 종료
락 획득
  &amp;darr; stock.lock.hold 시작
재고 조회 및 차감
락 반환 완료
  &amp;darr; stock.lock.hold 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과 해석 예시 1&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 작업 P95 = 900ms&lt;/li&gt;
&lt;li&gt;락 대기 P95 = 800ms&lt;/li&gt;
&lt;li&gt;락 점유 P95 = 70ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 잡은 뒤의 작업은 비교적 빠르지만, 여러 요청이 같은 락을 얻기 위해 오래 기다리는 상황으로 해석할 수 있다. 같은 키에 트래픽이 집중되는지, 락 범위가 너무 넓은지, 재시도가 경쟁을 늘리는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과 해석 예시 2&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 작업 P95 = 900ms&lt;/li&gt;
&lt;li&gt;락 대기 P95 = 20ms&lt;/li&gt;
&lt;li&gt;락 점유 P95 = 850ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 경쟁은 심하지 않지만 락 안의 DB 쿼리, 트랜잭션 또는 비즈니스 로직이 오래 걸리는 상황으로 해석할 수 있다. 임계 구역에 외부 API 호출이나 불필요한 작업이 포함됐는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시간만으로는 부족하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 획득 timeout의 횟수나 비율도 함께 확인해야 한다. Timer의 &lt;code&gt;result=timeout&lt;/code&gt; count를 이용하거나 사건 자체를 명확히 분리해야 한다면 Counter를 추가할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;주의 사항&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;전체 작업과 세부 작업은 목적에 맞게 측정한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 Timer로 전체 시간을 측정하면 어느 단계가 느린지 알기 어렵다.&lt;/li&gt;
&lt;li&gt;모든 메서드에 Timer를 붙이면 비용과 복잡도가 커진다.&lt;/li&gt;
&lt;li&gt;성능상 중요하거나 외부 의존성이 있거나 실제 병목 가능성이 있는 경계를 우선 측정한다.&lt;/li&gt;
&lt;li&gt;HTTP 요청 전체 시간은 Spring Boot 자동 계측이 이미 제공하는지 먼저 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;태그는 값의 종류가 제한적인 경우에만 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Meter는 이름과 태그 조합으로 식별된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result=success|failure&lt;/code&gt;, HTTP method처럼 값의 종류가 제한된 분류가 적절하다.&lt;/li&gt;
&lt;li&gt;userId, orderId, traceId, 이메일, 전체 query string은 태그로 사용하지 않는다.&lt;/li&gt;
&lt;li&gt;태그 값의 종류가 많아지는 &lt;b&gt;고카디널리티&lt;/b&gt;는 Meter 수, 메모리 사용량, 저장 비용을 급격히 늘릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Timer만으로 병목 원인을 확정할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Timer는 어느 구간이 얼마나 느린지 보여준다.&lt;/li&gt;
&lt;li&gt;왜 느린지는 로그, 분산 추적, 프로파일러, SQL 실행 계획, 스레드 덤프, Connection Pool 상태 등을 함께 분석해야 알 수 있다.&lt;/li&gt;
&lt;li&gt;Timer는 성능 저하를 발견하고 범위를 좁히는 도구이지 원인을 자동 진단하는 도구는 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;background-color: #f6e199;&quot;&gt;현재 프로젝트 범위에 맞는 선택을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 Spring Boot 인스턴스에서 k6로 통제된 성능 실험을 한다면 &lt;code&gt;publishPercentiles(0.5, 0.95, 0.99)&lt;/code&gt;로도 목적을 달성할 수 있다.&lt;/li&gt;
&lt;li&gt;다중 인스턴스의 값을 통합하거나 시간에 따른 변화를 저장하고 경보를 만들 필요가 생기면 histogram과 중앙 메트릭 시스템 도입 가치가 커진다.&lt;/li&gt;
&lt;li&gt;MSA라고 해서 Prometheus라는 특정 제품이 필수인 것은 아니다. 다만 분산된 서비스의 메트릭, 로그, 트레이스를 중앙에서 관찰할 체계는 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;핵심 정리&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Timer는 짧은 작업의 실행 시간과 실행 횟수를 함께 기록한다.&lt;/li&gt;
&lt;li&gt;MeterRegistry는 이름과 태그 조합으로 Meter를 등록하고 관리한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publishPercentiles()&lt;/code&gt;는 한 인스턴스가 자신의 측정 분포에서 percentile을 계산한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publishPercentileHistogram()&lt;/code&gt;은 bucket별 누적 개수를 노출해 여러 인스턴스의 분포를 합칠 수 있게 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;record()&lt;/code&gt;는 하나의 코드 블록, &lt;code&gt;Timer.Sample&lt;/code&gt;은 개발자가 정한 세부 구간, &lt;code&gt;@Timed&lt;/code&gt;는 메서드 전체 측정에 적합하다.&lt;/li&gt;
&lt;li&gt;락 성능은 대기 시간과 점유 시간을 분리해야 원인을 좁힐 수 있다.&lt;/li&gt;
&lt;li&gt;Timer는 병목을 보여주지만 원인을 자동으로 확정하지는 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/timers.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Timers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/histogram-quantiles.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Histograms and Percentiles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/registry.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/micrometer&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Baeldung Micrometer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://deep.ing9990.com/posts/micrometer-timer-essentials&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micrometer Timer 입문&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/573</guid>
      <comments>https://annovation.tistory.com/573#entry573comment</comments>
      <pubDate>Thu, 30 Jul 2026 22:03:42 +0900</pubDate>
    </item>
    <item>
      <title>[부하테스트] 재고 API - (2) k6 부하 테스트 환경과 개발 환경 분리</title>
      <link>https://annovation.tistory.com/575</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; font-weight: bold;&quot;&gt;진행 환경&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background: #f6e199;&quot;&gt;진행 환경&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Spring Boot 3.x&lt;/li&gt;
&lt;li&gt;MySQL 8&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Docker Compose&lt;/li&gt;
&lt;li&gt;k6&lt;/li&gt;
&lt;li&gt;Redisson&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; font-weight: bold;&quot;&gt;문제 상황&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;k6 재고 차감 부하 테스트를 구성하면서 테스트용 상품과 재고를 SQL로 직접 생성했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;시드 SQL 코드 리뷰 과정에서 기존에 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;product_id를 가진 재고가 다른&lt;span&gt;&amp;nbsp;&lt;/span&gt;stock_id로 저장되어 있다면 테스트 재고가 추가 삽입되어 하나의 상품에 여러 재고가 존재할 수 있다는 문제가 발견됐다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 시드 SQL만 수정하면 해결될 것이라 생각했지만 테스트 환경을 확인하면서 더 근본적인 문제가 있다는 것을 알게 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background: #f6e199;&quot;&gt;기존 환경&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용도MySQLRedis&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발 환경&lt;/td&gt;
&lt;td&gt;Docker MySQL (hubeleven)&lt;/td&gt;
&lt;td&gt;DB 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot 테스트&lt;/td&gt;
&lt;td&gt;Homebrew MySQL (hubeleven_test)&lt;/td&gt;
&lt;td&gt;DB 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k6 부하 테스트&lt;/td&gt;
&lt;td&gt;Docker MySQL (hubeleven)&lt;/td&gt;
&lt;td&gt;DB 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background: #f6e199;&quot;&gt;발견한 문제&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;k6 시드 데이터가 개발 데이터와 같은 스키마에 저장됐다.&lt;/li&gt;
&lt;li&gt;부하 테스트 중 개발 데이터가 변경되거나 삭제될 수 있었다.&lt;/li&gt;
&lt;li&gt;Spring Boot 테스트와 개발 서비스가 Redis DB 0을 공유했다.&lt;/li&gt;
&lt;li&gt;동일한 재고 락 키가 환경 간 충돌할 수 있었다.&lt;/li&gt;
&lt;li&gt;Spring Boot 테스트가 Homebrew MySQL과 Docker Redis를 혼합 사용했다.&lt;/li&gt;
&lt;li&gt;스키마명 대소문자 규칙이 일관되지 않았다.&lt;/li&gt;
&lt;li&gt;이전 테스트 데이터가 다음 테스트에 영향을 줄 수 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 시드 SQL의 중복 문제는 SQL 자체의 문제가 아니라 테스트 데이터와 개발 데이터가 동일한 저장 공간을 사용하는 환경 구성 문제와 연결되어 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; font-weight: bold;&quot;&gt;해결 방법&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background: #f6e199;&quot;&gt;환경 분리 방식 검토&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;환경별 컨테이너를 추가하는 방법과 하나의 컨테이너 안에서 논리적으로 분리하는 방법을 비교했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 프로젝트는 한 명이 로컬에서 개발, Spring Boot 테스트, k6 부하 테스트를 순차적으로 실행하는 구조이므로 자원 격리보다 데이터 격리가 더 중요했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가 컨테이너와 포트, 볼륨 관리 비용을 늘리지 않기 위해 MySQL은 스키마, Redis는 논리 DB를 사용하는 방식을 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용도MySQLRedis&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발&lt;/td&gt;
&lt;td&gt;hubeleven&lt;/td&gt;
&lt;td&gt;DB 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot 테스트&lt;/td&gt;
&lt;td&gt;hubeleven_test&lt;/td&gt;
&lt;td&gt;DB 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k6 부하 테스트&lt;/td&gt;
&lt;td&gt;hubeleven_loadtest&lt;/td&gt;
&lt;td&gt;DB 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL 스키마명은 운영체제별 대소문자 차이를 방지하기 위해 모두 소문자로 통일했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 Redisson은 커스텀 설정 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;spring.data.redis.database&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 자동 적용하지 않고 있었다. 따라서 설정값을 직접 주입하여 실제 Redis 연결에 반영하도록 수정했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;config.useSingleServer()
    .setAddress(&quot;redis://&quot; + host + &quot;:&quot; + port)
    .setDatabase(database);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; font-weight: bold;&quot;&gt;분리 후 장점과 단점&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background: #f6e199;&quot;&gt;장점&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 데이터와 테스트 데이터가 완전히 분리된다.&lt;/li&gt;
&lt;li&gt;Spring Boot 테스트가 개발용 Redis 락 키에 영향을 주지 않는다.&lt;/li&gt;
&lt;li&gt;k6 시드 SQL을 반복 실행해도 개발 데이터가 변경되지 않는다.&lt;/li&gt;
&lt;li&gt;Docker Compose 기준으로 테스트 환경을 통일할 수 있다.&lt;/li&gt;
&lt;li&gt;별도 컨테이너 없이 데이터 격리를 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;테스트 결과의 재현성이 높아졌다.&lt;/li&gt;
&lt;li&gt;환경별 설정이 명확해졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background: #f6e199;&quot;&gt;단점&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터만 분리되고 서버 자원은 공유된다.&lt;/li&gt;
&lt;li&gt;MySQL CPU, 메모리, 커넥션을 공유한다.&lt;/li&gt;
&lt;li&gt;Redis CPU와 메모리를 공유한다.&lt;/li&gt;
&lt;li&gt;개발 서비스와 k6를 동시에 실행하면 성능 측정에 영향을 줄 수 있다.&lt;/li&gt;
&lt;li&gt;Redis 논리 DB는 Cluster 환경에서는 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;DB 번호 설정 누락 여부를 별도로 검증해야 한다.&lt;/li&gt;
&lt;li&gt;기존 Docker 볼륨은 초기화 SQL이 다시 실행되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 성능 측정 시에는 다른 개발 작업이나 Spring Boot 테스트를 동시에 실행하지 않기로 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; font-weight: bold;&quot;&gt;문제 해결 결과&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;환경별 분리 결과&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;&lt;b&gt;환경&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;&lt;b&gt;MySQL&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;&lt;b&gt;Redis&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;개발&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;hubeleven&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DB 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Spring Boot 테스트&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;hubeleven_test&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DB 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;k6 부하 테스트&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;hubeleven_loadtest&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DB 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;환경별 분리 후 Gradle 전체 테스트 정상 통과&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-07-20 at 10.06.30 PM.png&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7ItkJ/dJMcajwhh15/KYtSOWkFokP3CTOIHtEdK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7ItkJ/dJMcajwhh15/KYtSOWkFokP3CTOIHtEdK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7ItkJ/dJMcajwhh15/KYtSOWkFokP3CTOIHtEdK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7ItkJ%2FdJMcajwhh15%2FKYtSOWkFokP3CTOIHtEdK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2508&quot; height=&quot;794&quot; data-filename=&quot;Screenshot 2026-07-20 at 10.06.30 PM.png&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;부하 테스트 환경 적용&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785139909208&quot; class=&quot;markdown&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;echo &quot;=== Config Server가 제공한 loadtest 설정 ===&quot;

curl -s http://localhost:8888/product-service/loadtest |
jq '{
  mysql: [.propertySources[].source[&quot;spring.datasource.url&quot;] | select(.)][0],
  redisDatabase: [.propertySources[].source[&quot;spring.data.redis.database&quot;] | select(.)][0]
}'

echo
echo &quot;=== 실제 애플리케이션 연결 확인 ===&quot;

docker exec mysql-hubeleven sh -c \
'mysql -uroot -p&quot;$MYSQL_ROOT_PASSWORD&quot; -N -e &quot;
SELECT CONCAT(
  \&quot;MySQL hubeleven_loadtest 연결 수: \&quot;,
  COUNT(*)
)
FROM information_schema.PROCESSLIST
WHERE DB = \&quot;hubeleven_loadtest\&quot;;
&quot;'

docker exec redis-hubeleven sh -c \
'printf &quot;Redis DB 2 연결 수: &quot;; redis-cli CLIENT LIST | grep -c &quot;db=2&quot;'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 명령어를 통해 MySQL 연결 대상과 Redis 연결 대상을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-07-20 at 10.20.34 PM.png&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn0PIC/dJMb998dGXQ/vw07HknOmaeVSenFzdQTLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn0PIC/dJMb998dGXQ/vw07HknOmaeVSenFzdQTLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn0PIC/dJMb998dGXQ/vw07HknOmaeVSenFzdQTLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn0PIC%2FdJMb998dGXQ%2Fvw07HknOmaeVSenFzdQTLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2314&quot; height=&quot;516&quot; data-filename=&quot;Screenshot 2026-07-20 at 10.20.34 PM.png&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;loadtest 프로필로 Product 서비스를 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1785139909208&quot; class=&quot;makefile&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;DB_USERNAME=&quot;$(sed -n 's/^DB_USERNAME=//p' .env)&quot; \
DB_PASSWORD=&quot;$(sed -n 's/^DB_PASSWORD=//p' .env)&quot; \
SPRING_PROFILES_ACTIVE=prod,loadtest \
SERVER_PORT=18085 \
./gradlew :product:bootRun&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Config Server 설정값과 실제 애플리케이션 연결 정보를 조회하여 MySQL은 hubeleven_loadtest 스키마, Redis는 DB 2에 정상적으로 연결된 것을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1785139909208&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/hubeleven_loadtest?...
  data:
    redis:
      database: 2&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 통해 부하 테스트 데이터와 Redis 키가 개발 환경과 분리되어 있음을 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; font-weight: bold;&quot;&gt;배운 점&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드와 시드 SQL이 정상 동작한다고 해서 테스트 결과의 신뢰성이 보장되는 것은 아니었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성능 테스트에서는 다음 조건을 함께 관리해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 데이터가 개발 데이터와 분리되어 있는가&lt;/li&gt;
&lt;li&gt;개선 전후 동일한 데이터와 인프라 조건을 사용하는가&lt;/li&gt;
&lt;li&gt;테스트 종료 후 상태가 다음 측정에 영향을 주지 않는가&lt;/li&gt;
&lt;li&gt;설정 파일의 값이 실제 클라이언트 연결에 반영되는가&lt;/li&gt;
&lt;li&gt;측정 결과가 코드 변경 때문인지 환경 차이 때문인지 구분 가능한가&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 성능 최적화 이전에 측정 환경의 재현성과 데이터 격리를 먼저 확보해야 한다는 점을 확인할 수 있었다.&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/575</guid>
      <comments>https://annovation.tistory.com/575#entry575comment</comments>
      <pubDate>Wed, 29 Jul 2026 23:20:10 +0900</pubDate>
    </item>
    <item>
      <title>[부하테스트] 재고 API - (1) k6로 테스트 해보기</title>
      <link>https://annovation.tistory.com/574</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;재고 차감 성능 문제 발견&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 재고 차감 로직은 분산락을 획득한 상태에서 다음 작업을 수행한다.&lt;/p&gt;
&lt;pre id=&quot;code_lock_hypothesis_01&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;락 획득
-&amp;gt; 상품 조회
-&amp;gt; 재고 조회
-&amp;gt; 재고 수량 검증
-&amp;gt; 재고 차감
-&amp;gt; 트랜잭션 커밋
-&amp;gt; 응답 데이터 생성
-&amp;gt; 락 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;재고 정합성을 위해 순차 처리가 필요한 핵심 구간은 재고 조회, 수량 검증, 차감 및 트랜잭션 커밋이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 상품명과 같은 상품 기본 정보 조회는 재고 수량의 정합성을 보장하기 위해 반드시 재고 락 안에서 실행할 필요는 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 상품 조회가 락 내부에 포함되면서 각 요청의 락 점유 시간이 길어지고, 뒤의 요청들이 락을 기다리는 시간도 함께 증가했을 가능성이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;가설&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;재고 정합성과 직접 관계없는 상품 조회와 응답 생성을 락 밖으로 이동하면 락 점유 시간이 감소하고, 동일 상품에 대한 동시 요청의 락 대기 시간과 API 응답시간도 개선될 것이다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 API 지연은 DB 커넥션 풀, SQL 실행, 애플리케이션 스레드 또는 로컬 실행 환경의 자원 부족 때문에 발생했을 가능성도 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 코드를 바로 수정하는 것이 아니라, 먼저 락 대기 시간과 점유 시간을 측정해 가설을 검증해야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;어떻게 테스트할 것인가?&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;외부 API 성능은 k6로 측정하고, 애플리케이션 내부 락 구간은 Micrometer Timer로 측정한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff;&quot;&gt;측정 대상&lt;/td&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff;&quot;&gt;측정 도구&lt;/td&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff;&quot;&gt;측정 지표&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;전체 재고 차감 API&lt;/td&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;TPS, avg, p95, p99, 실패율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;락 획득 대기&lt;/td&gt;
&lt;td&gt;Micrometer Timer&lt;/td&gt;
&lt;td&gt;평균, p95, 최대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;락 점유&lt;/td&gt;
&lt;td&gt;Micrometer Timer&lt;/td&gt;
&lt;td&gt;평균, p95, 최대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;재고 정합성&lt;/td&gt;
&lt;td&gt;k6 + DB 조회&lt;/td&gt;
&lt;td&gt;성공 요청 수와 실제 차감량 일치 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;락 획득 대기 시간&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_lock_measure_01&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;최초 tryLock() 호출 직전
-&amp;gt; 락 획득 성공 또는 최종 타임아웃&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;락 획득 대기 시간은 앞 요청이 락을 해제할 때까지 현재 요청이 기다린 시간을 의미한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;락 점유 시간&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_lock_measure_02&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;락 획득 성공
-&amp;gt; 재고 조회
-&amp;gt; 재고 차감
-&amp;gt; 트랜잭션 커밋
-&amp;gt; unlock() 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청마다 로그를 출력하면 로그 I/O가 부하 테스트 결과를 왜곡할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 Micrometer Timer를 사용해 요청 경로에서는 실행 시간을 메모리에 집계하고, 테스트 종료 후 집계된 결과만 확인한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;왜 k6를 사용하는가?&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단위 테스트와 동시성 테스트는 재고 수량이 정확하게 차감되는지 검증할 수 있지만, 요청이 증가할 때 응답시간과 처리량이 어떻게 변하는지는 확인하기 어렵다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;k6는 실제 HTTP 요청을 반복해서 전송하므로 Controller, 분산락, 트랜잭션, DB 처리와 응답 변환을 포함한 전체 API 성능을 측정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 상품에 동시 재고 차감 요청을 집중할 수 있다.&lt;/li&gt;
&lt;li&gt;VU 수를 고정해 리팩토링 전&amp;middot;후를 동일한 조건으로 비교할 수 있다.&lt;/li&gt;
&lt;li&gt;평균뿐 아니라 p95, p99와 같은 꼬리 지연을 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;TPS와 실패 요청 수를 함께 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;성공 요청 수와 실제 재고 차감량을 비교해 정합성도 검증할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 k6는 애플리케이션 외부에서 HTTP 응답을 관찰하기 때문에 락 대기와 점유 시간을 직접 측정할 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_lock_measure_03&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;k6
-&amp;gt; 사용자가 체감하는 전체 API 성능 측정

Micrometer Timer
-&amp;gt; 애플리케이션 내부 락 구간 측정&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 두 측정 결과를 함께 사용해야 API가 느려진 현상과 내부 원인의 연관성을 설명할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;왜 VU 200을 사용하는가?&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;explore 테스트에서 VU를 증가시켰을 때 VU 200 구간에서 처리량 증가가 정체되고 p95 응답시간이 크게 증가하는 현상이 확인됐다.&lt;/p&gt;
&lt;pre id=&quot;code_lock_vu_01&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;VU 증가
-&amp;gt; 동일 상품의 락 획득 요청 증가
-&amp;gt; 락 대기 요청 누적
-&amp;gt; p95 증가
-&amp;gt; TPS 증가 정체&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;VU 20 또는 VU 50에서는 락 대기가 충분히 발생하지 않아 락 내부 로직을 줄여도 개선 효과가 작게 나타날 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 병목이 드러난 VU 200을 고정 비교 지점으로 선택했다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;VU 200은 실제 사용자 200명을 의미하거나 서비스의 절대적인 처리 한계를 의미하지 않는다. 동일 상품에 요청을 집중시킨 리팩토링 전&amp;middot;후 비교 조건이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;리팩토링 전&amp;middot;후 검증 절차&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_lock_process_01&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;1. Micrometer Timer 적용

2. 리팩토링 전 VU 200 부하 실행
   - 락 대기 시간
   - 락 점유 시간

3. 측정 결과로 락 병목 가설 확인

4. 상품 조회 및 응답 생성을 락 밖으로 이동

5. 동시성 테스트로 재고 정합성 검증

6. 리팩토링 후 동일한 VU 200 부하 실행
   - k6 TPS, avg, p95, p99
   - 락 대기 시간
   - 락 점유 시간

7. 3회 측정 결과의 중앙값 비교&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 측정 전에는 Product 서비스를 재시작하고, 시드 SQL을 실행해 재고를 동일한 수량으로 초기화한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 VU와 실행 시간 사용&lt;/li&gt;
&lt;li&gt;동일한 MySQL 및 Redis 설정 사용&lt;/li&gt;
&lt;li&gt;동일한 상품 ID와 초기 재고 사용&lt;/li&gt;
&lt;li&gt;다른 서비스와 테스트를 동시에 실행하지 않음&lt;/li&gt;
&lt;li&gt;한 번의 좋은 결과를 선택하지 않고 3회 중앙값 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;k6 스크립트 구현&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;k6는 크게 두 가지 방식으로 부하를 만들 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Closed Model&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(vus&amp;nbsp;/&amp;nbsp;ramping-vus&amp;nbsp;executor)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 Iteration이 끝나야 다음 Iteration이 시작되는 모델&lt;br /&gt;(Iteration : Virtual User(VU)가 default() 함수를 한 번 처음부터 끝까지 실행하는 것을 의미)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&quot;The next iteration doesn't start until the previous one finishes.&quot;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;흐름 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1785139724006&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;VU 1 -&amp;gt; HTTP 요청 -&amp;gt; 응답 대기 -&amp;gt; 응답 완료 -&amp;gt; 다음 요청 시작&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;1244&quot; data-end=&quot;1257&quot;&gt;새로운 요청 시작 시점이 이전 요청이 언제 끝났는지에 의존한다.&lt;/li&gt;
&lt;li data-start=&quot;1244&quot; data-end=&quot;1257&quot;&gt;코드 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1785139724006&quot; class=&quot;&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;예시&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❗️&lt;/b&gt;문제점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부하가 없는 정상적인 서버의 응답시간이 100ms 라고 가정했을 때, 1초동안 약 10번의 요청을 보낼 수 있다.&lt;/li&gt;
&lt;li&gt;그런데 부하로 인해 서버가 느려지고 응답시간이 2초가 되었지만, closed model 은 응답을 받아야 VU가 다음 요청을 보낼 수 있기 때문에 오히려 부하가 줄어드는 현상이 생긴다.&lt;/li&gt;
&lt;li&gt;이 현상을 Coordinated Omission 이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&quot;Slower response times means longer iterations and a lower arrival rate.&quot;&lt;br /&gt;응답이 느려질수록 요청 도착률(Arrival Rate)도 함께 줄어드는 문제&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;흐름 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1785139724007&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;응답시간

100ms

&amp;darr;

1초에

10번 요청

----------------

응답시간

2초

&amp;darr;

2초에

1번 요청&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Open Model&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(constant-arrival-rate&amp;nbsp;/&amp;nbsp;ramping-arrival-rate&amp;nbsp;executor)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Closed Model의 문제를 해결하기 위해 나온 모델&lt;/li&gt;
&lt;li&gt;핵심은, 응답이 끝났는지와 관계없이 새로운 요청을 시작한다는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&quot;VU iterations arrive independently of iteration completion.&quot;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;흐름 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1785139724008&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;1초 -&amp;gt; 무조건 요청 시작 -&amp;gt; 응답 기다리지 않음 -&amp;gt; 2초 -&amp;gt; 또 요청 시작&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1785139724008&quot; class=&quot;&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;예시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❗️&lt;/b&gt;왜 Open Model이 필요할까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실제 서비스는 사용자가 응답이 느리다고 새로운 사용자가 안 들어오는 것은 아니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어, 네이버에서&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서, Open Model 이 현실 트래픽을 더 잘 모방한다고 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;/b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;비교&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff;&quot;&gt;Closed Model&lt;/td&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff;&quot;&gt;Open Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;요청 시작 기준&lt;/td&gt;
&lt;td&gt;이전 Iteration 종료 후 시작&lt;/td&gt;
&lt;td&gt;정해진 시간마다 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;응답시간 영향&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;받음&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;받지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;부하량&lt;/td&gt;
&lt;td&gt;응답이 느려질수록 감소&lt;/td&gt;
&lt;td&gt;일정하게 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;현실 트래픽 재현&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;상대적으로 낮음&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;대표 Executor&lt;/td&gt;
&lt;td&gt;constant-vus, ramping-vus&lt;/td&gt;
&lt;td&gt;constant-arrival-rate, ramping-arrival-rate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Open : 이 API의 최대 지속 가능 처리량(천장)은 어디인지 = 한계점 찾기(capacity test)&lt;/li&gt;
&lt;li&gt;Closed : 한계 안에서 동시성이 오를 때 어떻게 저하되고, 리팩토링으로 얼마나 개선됐는지 = 저하 특성 + 전후 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;프로젝트에 적용된 파일 구조&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_load_test_structure&quot; class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;product/load-test/
├── README.md
├── docs/
│   ├── scenario1-lock-design.md
│   └── test-environment-isolation-design.md
├── results/
│   └── baseline-summary.json
├── scripts/
│   └── scenario1-stock-decrease-lock.js
└── seed/
    └── seed-scenario1-lock.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;README.md&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부하 테스트 환경 구성 방법을 기록한다.&lt;/li&gt;
&lt;li&gt;MySQL, Redis, Eureka, Config Server, Product 서비스 실행 순서를 설명한다.&lt;/li&gt;
&lt;li&gt;시드 SQL과 k6 스크립트 실행 방법을 제공한다.&lt;/li&gt;
&lt;li&gt;리팩토링 전&amp;middot;후 동일하게 적용해야 하는 측정 절차를 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;docs/scenario1-lock-design.md&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 상품에 재고 차감 요청을 집중시키는 테스트 시나리오를 설명한다.&lt;/li&gt;
&lt;li&gt;explore 모드와 compare 모드를 분리한 이유를 기록한다.&lt;/li&gt;
&lt;li&gt;측정할 k6 지표와 재고 정합성 검증 기준을 정의한다.&lt;/li&gt;
&lt;li&gt;VU 200을 리팩토링 전&amp;middot;후 고정 비교 지점으로 선택한 근거를 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;docs/test-environment-isolation-design.md&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발, Spring Boot 테스트, k6 부하 테스트의 데이터 저장 공간을 분리한 이유를 설명한다.&lt;/li&gt;
&lt;li&gt;MySQL 스키마와 Redis 논리 DB를 환경별로 분리한 구조를 기록한다.&lt;/li&gt;
&lt;li&gt;별도 컨테이너 대신 논리적 격리를 선택한 근거와 한계를 정리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_load_test_isolation&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;개발 환경
├── MySQL: hubeleven
└── Redis: DB 0

Spring Boot 테스트
├── MySQL: hubeleven_test
└── Redis: DB 1

k6 부하 테스트
├── MySQL: hubeleven_loadtest
└── Redis: DB 2&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;results/baseline-summary.json&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VU 200 compare 테스트를 3회 실행한 결과를 저장한다.&lt;/li&gt;
&lt;li&gt;각 실행의 TPS, 평균 응답시간, p95, p99를 기록한다.&lt;/li&gt;
&lt;li&gt;한 번의 결과를 선택하지 않고 3회 측정값의 중앙값을 공식 baseline으로 사용한다.&lt;/li&gt;
&lt;li&gt;성공 요청 수와 실제 재고 차감량이 일치했는지 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;scripts/scenario1-stock-decrease-lock.js&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 상품 ID에 재고 차감 요청을 집중시키는 k6 스크립트다.&lt;/li&gt;
&lt;li&gt;Closed Model의 &lt;code&gt;constant-vus&lt;/code&gt; executor를 사용한다.&lt;/li&gt;
&lt;li&gt;explore 모드에서는 VU 20, 50, 100, 200 구간을 순차적으로 실행한다.&lt;/li&gt;
&lt;li&gt;compare 모드에서는 선택한 VU를 고정해 리팩토링 전&amp;middot;후를 비교한다.&lt;/li&gt;
&lt;li&gt;성공, 락 타임아웃, 예상하지 못한 실패를 별도 메트릭으로 집계한다.&lt;/li&gt;
&lt;li&gt;테스트 전후 재고를 조회해 성공 요청 수와 실제 차감량을 비교한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_load_test_modes&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;explore
-&amp;gt; VU 20
-&amp;gt; VU 50
-&amp;gt; VU 100
-&amp;gt; VU 200
-&amp;gt; 병목이 나타나는 비교 지점 탐색

compare
-&amp;gt; VU 200 고정
-&amp;gt; 1분 측정
-&amp;gt; 리팩토링 전&amp;middot;후 동일 조건 비교&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;seed/seed-scenario1-lock.sql&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고 차감 부하 테스트에 사용할 상품과 재고 데이터를 생성한다.&lt;/li&gt;
&lt;li&gt;고정 UUID를 사용해 k6 스크립트가 항상 동일한 상품을 요청하도록 한다.&lt;/li&gt;
&lt;li&gt;테스트 중 재고가 고갈되지 않도록 충분한 초기 재고를 설정한다.&lt;/li&gt;
&lt;li&gt;기존 테스트 데이터를 삭제한 후 다시 삽입해 여러 번 실행해도 같은 초기 상태를 만든다.&lt;/li&gt;
&lt;li&gt;개발 데이터와 분리된 &lt;code&gt;hubeleven_loadtest&lt;/code&gt; 스키마만 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;현재는 재고 차감 락 테스트인 시나리오 1만 구현되어 있다. 조회 쿼리 및 Redis Cache 성능 테스트인 시나리오 2는 이후 별도 스크립트와 시드 데이터로 추가할 예정이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Grafana Docs : Open and closed models&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot;&gt;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1785139731664&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Open and closed models | Grafana k6 documentation&quot; data-og-description=&quot;Open and closed models Different k6 executors have different ways of scheduling VUs. Some executors use the closed model, while the arrival-rate executors use the open model. In short, in the closed model, VU iterations start only when the last iteration f&quot; data-og-host=&quot;grafana.com&quot; data-og-source-url=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot; data-og-url=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/buIFLh/dJMb896hyvW/V1kjxpbh8Sf9ITCbNpot30/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/gLRTL/dJMb8XSjKGj/3G5ve0c4jN1jjkxgZYolb1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/buIFLh/dJMb896hyvW/V1kjxpbh8Sf9ITCbNpot30/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/gLRTL/dJMb8XSjKGj/3G5ve0c4jN1jjkxgZYolb1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Open and closed models | Grafana k6 documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Open and closed models Different k6 executors have different ways of scheduling VUs. Some executors use the closed model, while the arrival-rate executors use the open model. In short, in the closed model, VU iterations start only when the last iteration f&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;grafana.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/574</guid>
      <comments>https://annovation.tistory.com/574#entry574comment</comments>
      <pubDate>Tue, 28 Jul 2026 22:37:35 +0900</pubDate>
    </item>
    <item>
      <title>[부하테스트] 부하 테스트 란? (feat. k6)</title>
      <link>https://annovation.tistory.com/568</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;중요한 개념&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;100&quot;&gt;처리량 (Throughput)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정의 : 단위 시간당 시스템이 처리하는 요청의 양&lt;/li&gt;
&lt;li&gt;지표 : 주로 TPS(Transactions Per Second)나 RPS(Requests Per Second)로 측정한다.&lt;/li&gt;
&lt;li&gt;의미 : 시스템이 얼마나 많은 일을 할 수 있는지 성능의 '양'을 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;257&quot;&gt;지연 시간 (Latency)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정의 : 사용자가 요청을 보낸 시점부터 응답을 받을 때까지 걸리는 시간&lt;/li&gt;
&lt;li&gt;의미 : 사용자에게 얼마나 빠른 '경험'을 주는지 성능의 '질'을 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;257&quot;&gt;&lt;b data-start-index=&quot;356&quot;&gt;상관관계&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부하가 증가해도 일정 수준까지는 처리량이 늘어나지만, 임계치를 넘으면 지연 시간이 급격히 증가하고 처리량은 정체되거나 하락한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;부하 테스트 툴 선정 (k6)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;K6 란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;k6 백엔드 API나 웹 서비스에 &lt;span&gt;가상의 사용자, 즉 Virtual User를 만들어 반복적으로 요청을 보내고, 응답 시간, 실패율, 처리량 같은 성능 지표를 측정하는 성능 테스트 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;K6 선정 이유&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설치가 간단하다 (macOS 기준 brew install 만 해주면 된다.)&lt;/li&gt;
&lt;li&gt;학습 곡선이 적다. (javascript로 작성하므로, 친숙한 언어로 빠르게 학습하여 적용할 수 있다.)&lt;/li&gt;
&lt;li&gt;메모리를 적게 쓰면서도 많은 트래픽을 효율적으로 발생시킬 수 있으며, 사용법이 간단하면서도 테스트 결과가 매우 정확하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️단순히 테스트 툴의 사용법을 아는 것보다, 결과 데이터를 정확하게 해석하고 이를 바탕으로 시스템 성능을 개선할 수 있는 능력이 실무에서 훨씬 중요!&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;부하 테스트를 통한 결과 해석&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;병목 지점 (Bottleneck Point) 발견 가능&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1. 병목 지점(Bottleneck Point) 이란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개념 : 전체 시스템 인프라 구성에서 특정 서버 자원(CPU, 메모리 등)이 한계에 도달하여, 전체 성능이 저하되는 구간&lt;/li&gt;
&lt;li&gt;고속도로 비유 : 넓은 3차선 도로를 달리던 차들이 갑자기 2차선으로 좁아지는 구간이나 여러 도로가 합쳐지는 구간에 진입할 때 발생하는 '교통 정체 구간'과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;시스템 성능 개선의 핵심 원칙&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템의 성능을 개선하려면 반드시 정체가 발생한 병목 지점(2차선 구간)을 해결해야 한다.&lt;/li&gt;
&lt;li&gt;정체가 없는 원활한 구간(3차선 도로)을 아무리 크게 넓혀봐야 좁은 구간의 정체가 나아지지 않으며, 오히려 더 많은 차들이 좁은 구간으로 몰려들어 상황이 더 나빠지게 된다.&lt;/li&gt;
&lt;li&gt;즉, 아무 곳이나 성능을 개선한다고 해서 전체 시스템의 성능이 올라가는 것이 아니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;병목 지점의 스루풋(Throughput) = 전체 스루풋&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리량이 제일 낮은 병목 지점의 스루풋이 곧 전체 시스템의 스루풋이 된다.&lt;/li&gt;
&lt;li&gt;ex.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 백엔드 서버가 1초당 1,000개의 요청을 처리할 수 있더라도, DB가 1초당 300개의 요청만 처리할 수 있다면 전체 시스템의 1초당 처리량은 결국 300개이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;병목 지점 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;해소 시 나타나는 새로운 현상&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 병목 지점을 해소하면, 다른 곳에서 새로운 병목 지점이 발생한다는 사실도 명심해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bob49q/dJMcabkypwh/cn7UX9VMf6ReDGk6DTqgl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bob49q/dJMcabkypwh/cn7UX9VMf6ReDGk6DTqgl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bob49q/dJMcabkypwh/cn7UX9VMf6ReDGk6DTqgl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbob49q%2FdJMcabkypwh%2Fcn7UX9VMf6ReDGk6DTqgl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1027&quot; height=&quot;693&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;부하 테스트 전체 흐름&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;1. &lt;b data-start-index=&quot;155&quot;&gt;부하 테스트의 필요성 인식&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'사용자들이 몰려서 서버가 터지면 어떡하지?' 또는 '내 서버는 과연 어느 정도의 사용자 요청을 견딜 수 있을까?' 와 같은 궁금증이 생길 때 부하 테스트가 필요하다.&lt;/li&gt;
&lt;li&gt;ex. 선착순 쿠폰 발급 API 성능에 대해 궁금할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;2. &lt;b data-start-index=&quot;442&quot;&gt;부하 테스트 목표 설정&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;막연하게 테스트하는 것이 아니라, 스루풋(Throughput, TPS)과 레이턴시(Latency)를 기준으로 명확한 목표치를 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYvUWb/dJMcaaeQ1MN/LCLv0u72PLxdsBQid3MwR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYvUWb/dJMcaaeQ1MN/LCLv0u72PLxdsBQid3MwR0/img.png&quot; data-alt=&quot;부하 테스트 목표 설정 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYvUWb/dJMcaaeQ1MN/LCLv0u72PLxdsBQid3MwR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYvUWb%2FdJMcaaeQ1MN%2FLCLv0u72PLxdsBQid3MwR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;206&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;부하 테스트 목표 설정 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;3. &lt;b data-start-index=&quot;690&quot;&gt;부하 테스트 실행 및 현재 상태 측정&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 시스템이 어느 정도의 스루풋과 레이턴시를 감당할 수 있는지 부하 테스트 툴을 활용해 직접 측정&lt;/li&gt;
&lt;li&gt;측정 방식 : 처음부터 엄청난 부하를 주는 것이 아니라, 부하의 정도를 점점 올려가면서 시스템이 어느 정도까지 견디는지 확인하고 최대 스루풋을 파악하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;4. &lt;b data-start-index=&quot;908&quot;&gt;병목 지점 파악&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능을 올리려면 반드시 병목 지점을 파악해서 개선해야 한다.&lt;/li&gt;
&lt;li&gt;시스템 아무 곳이나 개선한다고 전체 성능이 좋아지는 것이 아니며, 오직 병목 지점을 개선해야만 전체 성능이 올라간다는 것을 기억해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;5. &lt;b data-start-index=&quot;1164&quot;&gt;성능 개선 및 재테스트 (목표 달성까지 반복)&lt;/b&gt;&lt;span style=&quot;background-color: #22262b; color: #c7c7c7; text-align: start;&quot; data-start-index=&quot;1192&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;병목 지점을 찾아 개선했다면, 개선된 시스템이 트래픽을 얼마나 더 견디는지 확인하기 위해 다시 부하 테스트를 진행한다.&lt;/li&gt;
&lt;li&gt;재테스트 : 이전과 똑같이 부하를 점점 올려가며 최대 스루풋과 평균 레이턴시를 다시 측정한다.&lt;/li&gt;
&lt;li&gt;반복 작업 : 만약 성능을 개선했는데도 측정 결과가 1,900 TPS가 나와서 여전히 목표치(2,000 TPS)에 도달하지 못했다면, 다시 또 다른 병목 지점을 파악하고 성능을 개선하는 작업을 반복해야 한다.&lt;/li&gt;
&lt;li&gt;계속해서 [병목 지점 파악 &amp;rarr; 성능 개선 &amp;rarr; 부하 테스트] 사이클을 돌다가, 목표치를 달성하게 되면 그때 부하 테스트 과정을 마무리하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;주의점&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;적절한 부하 테스트 시간 확보 (최소 5분 이상)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주로 1분 간격으로 지표를 기록하기 때문에, 정확한 성능 측정을 위해 최소 5분간 부하테스트를 진행해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;프로덕션 환경과 동일한 데이터 세팅&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스의 성능은 데이터가 어떻게 저장되어 있는지, 그리고 데이터가 얼마나 많이 저장되어 있는지(데이터 양)에 따라 엄청난 차이가 발생한다.&lt;/li&gt;
&lt;li&gt;따라서 보다 정확한 결과값을 얻으려면, 실제 서비스 중인 프로덕션(운영) 환경과 비슷하게 데이터를 구성해두고 테스트를 진행해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start; background-color: #f6e199;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;프로덕션과 분리된 환경에서 테스트&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 서비스가 돌아가고 있는 프로덕션 환경에서 직접 부하 테스트를 진행하게되면, 부하 테스트 자체가 인프라에 극한의 트래픽을 가하는 행위이므로, 자칫하면 실제 서비스에 장애를 유발할 수 있기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;부하 테스트 목표 : 성능 개선&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;220&quot;&gt;성능 개선을 위한 3가지 필수 요소&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 트래픽 처리를 위해 시스템을 고도화할 때, 스루풋(Throughput)과 레이턴시(Latency)를 고려하게 되는데,&lt;/li&gt;
&lt;li&gt;시스템을 안정적으로 개선하기 위해서는 '가용성(Availability)'이라는 요소도 반드시 함께 고려하며 설계해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;394&quot;&gt;가용성(Availability)이란?&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개념 : 시스템이 장애 없이 서비스를 정상적으로 제공할 수 있는 가능성(확률)&lt;/li&gt;
&lt;li&gt;가용성이 높고 낮음
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;언제 접속해도 매번 잘 작동하여 장애 발생 가능성이 극히 작은 시스템을 '가용성이 높은 시스템'이라 하고, 반대로 서비스가 다운되는 시간(다운타임)이 긴 시스템을 '가용성이 낮은 시스템'이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정상 가동률(퍼센트 표기)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;가용성은 퍼센트(%)로 표시한다.&lt;/li&gt;
&lt;li&gt;예를 들어 가용성 99.99%라는 것은 1년 중 99.99%의 시간 동안 정상 이용이 가능하며, 서비스가 다운되는 시간은 1년에 단 53분 정도밖에 되지 않는 시스템을 뜻한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가용성이 높은 시스템을 만들려면, 서비스가 사용 불가능한 시간(다운 타임)을 아예 발생시키지 않거나, 발생하더라도 그 시간이 아주 짧게 지나가도록 설계해야한다.&lt;/li&gt;
&lt;li&gt;다운 타임을 줄이는 데 가장 중요한 설계 방법 중 하나가 시스템 이중화(System Redundancy) 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;394&quot;&gt;&lt;b data-start-index=&quot;824&quot;&gt;시스템 이중화 (System Redundancy)&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개념 : 다운타임을 줄이고 가용성을 높이기 위한 가장 중요한 설계 방법 중 하나이다. 시스템의 일부분을 사용할 수 없게 고장 나더라도, 다른 시스템을 이용하여 서비스를 계속 제공할 수 있게 만드는 것을 의미한다.&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;A, B, C라는 3대의 서버가 있을 때, A가 고장 나도 B와 C를 쓰고, B가 고장 나도 C를 쓸 수 있도록 구성하는 것&lt;/li&gt;
&lt;li&gt;세 대가 한 번에 동시에 고장 날 확률은 아주 적기 때문에 서비스가 계속 유지될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;394&quot;&gt;&lt;b data-start-index=&quot;824&quot;&gt;&lt;b data-start-index=&quot;1249&quot;&gt;인프라 설계에서의 이중화 적용 (ELB와 EC2)&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;477&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFHe78/dJMcaiX84wi/vvcyETUtkfyb1CUFZN5eyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFHe78/dJMcaiX84wi/vvcyETUtkfyb1CUFZN5eyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFHe78/dJMcaiX84wi/vvcyETUtkfyb1CUFZN5eyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFHe78%2FdJMcaiX84wi%2FvvcyETUtkfyb1CUFZN5eyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;477&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;477&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 서버 구조의 한계
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;사용자가 ELB(로드밸런서)를 통해 단 1대의 EC2 서버로 통신하는 구조가 있다고 한다면, 만약 이 1대의 서버가 멈춰버리면, 그 순간 사용자는 곧바로 서비스 장애를 경험하게 된다. (가용성이 낮은 설계)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이중화 서버 구조의 장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;EC2 서버를 2대 이상으로 구성하면, 1대가 고장 나더라도 살아있는 나머지 1대의 서버가 정상적으로 통신을 이어가게되고, 사용자 입장에서는 서비스가 중단되었다는 사실조차 느낄 수 없게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결론 : 안정적인 서비스 운영을 위해서는 이처럼 최대한 시스템 이중화를 적용하여 가용성을 높이는 방향으로 인프라를 설계해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;성능 개선 3가지 전략&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하 테스트를 통해 병목 지점을 찾았다면, 이를 해결하고 시스템을 고도화하기 위해 주로 아래의 세 가지 방법을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;275&quot;&gt;1. 수평적 확장 (Horizontal Scaling)&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 시스템의 성능을 올리기 위해 시스템(컴퓨터/서버)의 '개수'를 늘리는 방식&lt;/li&gt;
&lt;li&gt;예를 들어 EC2 서버 1대로 요청을 처리하기에 CPU나 메모리 리소스가 모자랄 때, 컴퓨터를 한 대 더 추가하는 것&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;가용성 증가 : 서버가 여러 대가 되므로 자연스럽게 '시스템 이중화'가 되어 장애가 발생할 확률이 줄어든다.&lt;/li&gt;
&lt;li&gt;무한한 확장성 : 컴퓨터 개수를 100개, 1,000개, 10,000개로 계속 늘릴 수 있어 확장에 제한이 없다.&lt;/li&gt;
&lt;li&gt;유연성 : 필요에 따라 서버를 쉽게 추가하고 또 금방 없앨 수 있어 축소 및 확장이 매우 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;여러 대의 서버를 한 대의 서버처럼 사용하기 위한 추가 작업이 필요하다. ex. 로드 밸런서&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;275&quot;&gt;2. &lt;b data-start-index=&quot;691&quot;&gt;수직적 확장 (Vertical Scaling)&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템의 개수를 늘리는 것이 아니라, 기존 시스템(컴퓨터)의 스펙(CPU, 메모리 등)을 더 좋은 사양으로 업그레이드(교체)하는 방식&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;단순한 인프라 : 서버가 단 한 대뿐이므로 로드 밸런서(ELB) 같은 추가 작업이 필요 없다.&lt;/li&gt;
&lt;li&gt;비용 절감 : 인프라 구성이 단순하여 금전적인 비용뿐만 아니라 시간과 노력 등 전반적인 관리 비용이 적게 든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;가용성 감소(단일 장애 지점) : 서버가 단 한 대뿐이라 시스템 이중화가 안 되어있기 때문에, 이 서버가 고장 나면 서비스 전체가 중단된다.&lt;/li&gt;
&lt;li&gt;확장의 한계 : 컴퓨터 부품의 최고 사양에는 물리적인 한계(ex. 꽂을 수 있는 메모리의 최대치)가 존재하므로 확장성에 명확한 제한이 있다.&lt;/li&gt;
&lt;li&gt;다운타임 발생 : 스펙을 업그레이드하려면 기존 컴퓨터를 끄고 부품을 교체해야 하므로 불가피하게 서비스 중단(다운타임)이 발생하며, 이 역시 가용성을 떨어뜨리는 요인이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;275&quot;&gt;3. &lt;b data-start-index=&quot;1177&quot;&gt;캐싱 (Caching)&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 조회할 때, 원래 데이터가 있는 곳보다 더 빠르게 액세스(접근)할 수 있는 곳에 데이터를 '임시로 저장'해두고 가져오는 방식&lt;/li&gt;
&lt;li&gt;예시 1 : &lt;b&gt;Redis&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btczWC/dJMcaf1srQe/21l4Y7XKyijxAffhNKjcBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btczWC/dJMcaf1srQe/21l4Y7XKyijxAffhNKjcBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btczWC/dJMcaf1srQe/21l4Y7XKyijxAffhNKjcBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtczWC%2FdJMcaf1srQe%2F21l4Y7XKyijxAffhNKjcBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;764&quot; height=&quot;448&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자주 접근하는 데이터를 DB 보다 훨씬 속도가 빠른 임시 저장소인 '레디스(Redis)'에 일부 저장해 두고 가져오면 성능을 크게 끌어올릴 수 있다.&lt;/li&gt;
&lt;li&gt;예시 2 : &lt;b data-start-index=&quot;1427&quot;&gt;CDN - CloudFront&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzuxc/dJMcag0lQLf/h0lSpTuIpRv5kwegik3RP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzuxc/dJMcag0lQLf/h0lSpTuIpRv5kwegik3RP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzuxc/dJMcag0lQLf/h0lSpTuIpRv5kwegik3RP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkzuxc%2FdJMcag0lQLf%2Fh0lSpTuIpRv5kwegik3RP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;577&quot; height=&quot;600&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS S3에 동영상 원본이 저장되어 있을 때, 물리적으로 거리가 먼 나라의 사용자는 다운로드 속도가 오래 걸려 성능 악화를 경험하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 해결하기 위해 각 나라별로 가까운 곳에 임시 저장소를 두고 원본 동영상을 '복제'해 둔다.&lt;/li&gt;
&lt;li&gt;사용자는 거리가 먼 S3 원본이 아니라 자신과 가장 가까운 복제 저장소에서 데이터를 빠르게 가져올 수 있는데, 이 CDN(Content Delivery Network) 기법 역시 캐싱 전략의 일종이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;트래픽 증가에 따른 시스템 설계 및 확장 방법&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하 테스트를 통해 설정한 목표 스루풋과 레이턴시를 달성하려면 병목 지점을 찾고 그에 맞는 적절한 확장을 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병목 지점이 어디서 발생하느냐에 따라 성능 개선 방법이 달라지는데, 이 방법에 대해 대략적으로 알아놓으면 성능 개선을 수월하게 진행할 수 있다.&lt;/p&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;377&quot;&gt;&lt;span data-start-index=&quot;377&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;380&quot;&gt;1단계 : 단일 서버 구성 (모든 것을 한 곳에)&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;377&quot;&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;380&quot;&gt;&lt;/b&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CmijZ/dJMcaiDV3B9/Q5vymU0Jd45kEkjYNfmnbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CmijZ/dJMcaiDV3B9/Q5vymU0Jd45kEkjYNfmnbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CmijZ/dJMcaiDV3B9/Q5vymU0Jd45kEkjYNfmnbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCmijZ%2FdJMcaiDV3B9%2FQ5vymU0Jd45kEkjYNfmnbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;585&quot; height=&quot;176&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조 : EC2 서버 단 한 대에 프론트엔드, 백엔드, 데이터베이스(DB)를 모두 설치해서 운영하는 가장 심플한 형태&lt;/li&gt;
&lt;li&gt;장점 : 여러 리소스를 쓰지 않아 비용이 적게 들고, 서버 한 대만 조작하면 되므로 관리가 아주 쉽다.&lt;/li&gt;
&lt;li&gt;문제점(병목) : 서비스가 커지면 DB가 CPU, 메모리, 디스크 등 컴퓨터 자원을 엄청나게 잡아먹기 시작하면서, 같은 서버에 있는 웹 애플리케이션의 성능까지 함께 떨어뜨리는 악영향을 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;638&quot;&gt;&lt;span data-start-index=&quot;638&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;641&quot;&gt;2단계 : 데이터베이스(DB) 서버 분리&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;638&quot;&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;641&quot;&gt;&lt;/b&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l1LaH/dJMcaf8c3Dx/sZGCF0A2KcKJFat4GkIi80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l1LaH/dJMcaf8c3Dx/sZGCF0A2KcKJFat4GkIi80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l1LaH/dJMcaf8c3Dx/sZGCF0A2KcKJFat4GkIi80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl1LaH%2FdJMcaf8c3Dx%2FsZGCF0A2KcKJFat4GkIi80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;148&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조 : 1단계의 문제를 해결하기 위해 웹 애플리케이션(EC2)과 데이터베이스(RDS)를 별도의 서버로 분리한다.&lt;/li&gt;
&lt;li&gt;문제점(병목) : 트래픽이 더 늘어나면, 용량이 큰 '정적 파일(이미지, 동영상 등)'을 제공하는 과정에서 웹 서버가 자원을 너무 많이 소모하여 과부하에 걸리게 된다. (단순 HTML, CSS, JS는 용량이 작지만, 이미지나 영상은 서버에 큰 부담을 준다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;871&quot;&gt;&lt;span data-start-index=&quot;871&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;874&quot;&gt;3단계 : 정적 파일 서버(S3) 분리&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;871&quot;&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;874&quot;&gt;&lt;/b&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QwMa3/dJMcahZhMKE/W9hs5lyk5Ua49obZWuwlM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QwMa3/dJMcahZhMKE/W9hs5lyk5Ua49obZWuwlM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QwMa3/dJMcahZhMKE/W9hs5lyk5Ua49obZWuwlM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQwMa3%2FdJMcahZhMKE%2FW9hs5lyk5Ua49obZWuwlM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;582&quot; height=&quot;290&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조 : 웹 서버의 과부하를 막기 위해 이미지, 동영상 같은 정적 파일만 전담해서 제공하는 별도의 서버(AWS S3)를 구축한다.&lt;/li&gt;
&lt;li&gt;문제점(병목) : 정적 파일을 분리하긴 했지만, 넷플릭스를 볼 때처럼 사용자와 데이터를 제공하는 원본 서버(S3)의 물리적 거리가 멀면 응답 속도(다운로드)가 매우 느려지는 문제가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;1072&quot;&gt;&lt;span data-start-index=&quot;1072&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;1075&quot;&gt;4단계 : CDN (CloudFront) 도입&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;1072&quot;&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;1075&quot;&gt;&lt;/b&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKBurC/dJMcahygjvG/8TtmL9QP7CIWAphvnlOyOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKBurC/dJMcahygjvG/8TtmL9QP7CIWAphvnlOyOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKBurC/dJMcahygjvG/8TtmL9QP7CIWAphvnlOyOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKBurC%2FdJMcahygjvG%2F8TtmL9QP7CIWAphvnlOyOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;583&quot; height=&quot;308&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조 : 물리적 거리로 인한 지연을 해결하기 위해 캐싱 원리가 적용된 CDN(AWS CloudFront)을 도입한다.&lt;/li&gt;
&lt;li&gt;전 세계 곳곳에 임시 저장소를 두고 원본 데이터를 복제해 두어, 사용자가 가장 가까운 곳에서 파일을 빠르게 다운받을 수 있게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;1243&quot;&gt;&lt;span data-start-index=&quot;1243&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;1246&quot;&gt;5단계 : 웹 서버(EC2)의 수평적 확장과 로드 밸런서(ELB)&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;1243&quot;&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;1246&quot;&gt;&lt;/b&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/otGRm/dJMcaa0a6xV/L9Ci5EpyJkkCkTzOyYdeZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/otGRm/dJMcaa0a6xV/L9Ci5EpyJkkCkTzOyYdeZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/otGRm/dJMcaa0a6xV/L9Ci5EpyJkkCkTzOyYdeZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FotGRm%2FdJMcaa0a6xV%2FL9Ci5EpyJkkCkTzOyYdeZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;448&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트래픽이 더 늘어 EC2 자체의 성능이 모자라 병목이 발생하면 확장이 필요하다.&lt;/li&gt;
&lt;li&gt;가용성(시스템 이중화)의 이점을 얻기 위해 EC2 서버의 개수를 늘리는 '수평적 확장' 방식을 주로 선택한다. (물론 컴퓨터 사양을 높이는 수직적 확장을 병행할 수도 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nfdJF/dJMcagMWuw5/eQtNyHyAKBkfX0yKLJiOsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nfdJF/dJMcagMWuw5/eQtNyHyAKBkfX0yKLJiOsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nfdJF/dJMcagMWuw5/eQtNyHyAKBkfX0yKLJiOsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnfdJF%2FdJMcagMWuw5%2FeQtNyHyAKBkfX0yKLJiOsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;366&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로드 밸런서(ELB) 필수 도입 : 서버가 여러 대가 되면 사용자에게 '알아서 나눠서 접속해라'라고 할 수 없으므로, 트래픽을 각 서버에 골고루 공평하게 분배해 주는 로드 밸런서를 반드시 도입해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;1544&quot;&gt;&lt;span data-start-index=&quot;1544&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;1547&quot;&gt;6단계 : 데이터베이스(RDS)의 성능 개선 및 확장&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;1544&quot;&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b data-start-index=&quot;1547&quot;&gt;&lt;/b&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byCNrq/dJMcaixaWuJ/W3nZKkIdgid99eAX4kkCak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byCNrq/dJMcaixaWuJ/W3nZKkIdgid99eAX4kkCak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byCNrq/dJMcaixaWuJ/W3nZKkIdgid99eAX4kkCak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyCNrq%2FdJMcaixaWuJ%2FW3nZKkIdgid99eAX4kkCak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;582&quot; height=&quot;353&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z9aAy/dJMcai4UVQe/dkR8glXkiIKiBdIpIkkuD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z9aAy/dJMcai4UVQe/dkR8glXkiIKiBdIpIkkuD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z9aAy/dJMcai4UVQe/dkR8glXkiIKiBdIpIkkuD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz9aAy%2FdJMcai4UVQe%2FdkR8glXkiIKiBdIpIkkuD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;589&quot; height=&quot;424&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 서버가 늘어나면 수많은 요청이 DB로 몰리며 결국 DB에서 병목 현상이 발생한다.&lt;/li&gt;
&lt;li&gt;DB의 병목은 아래의 순서대로 해결을 시도해야 한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB 자체 튜닝 (최우선) : 무작정 서버를 늘리기 전에 인덱스(Index) 활용, SQL 쿼리문 튜닝, 역정규화(테이블 설계 변경) 등을 통해 DB 내부적으로 먼저 성능 개선을 시도&lt;/li&gt;
&lt;li&gt;수직적 확장 (Scale-up) : 튜닝으로도 안 되면 DB 컴퓨터의 사양을 높인다. (주의 : DB를 여러 대 두는 '수평적 확장'은 각 DB 간의 '데이터 동기화' 과정이 필수적인데, 이 동기화 작업이 오히려 DB 성능을 심각하게 저하시키기 때문에 우선순위에서 밀린다.)&lt;/li&gt;
&lt;li&gt;읽기 전용 DB (Read Replica) 도입 : 실시간으로 완벽한 데이터 동기화가 필요 없는 쿼리(ex. 작성 후 몇 초 뒤에 보여도 상관없는 게시글 조회)를 분담시키기 위해 '읽기 전용 데이터베이스'를 추가로 도입하여 조회 트래픽을 분산시킨다.&lt;/li&gt;
&lt;li&gt;캐시 서버 (Redis 등) 도입 : DB 사양을 계속 높이면 한 달에 천만 원 이상이 나갈 정도로 비용이 너무 비싸지므로. 비용 절감과 조회 속도 향상을 위해 데이터 조회 요청을 DB와 나눠서 처리해 주는 '캐시 서버'를 도입하여 DB의 부하를 줄이고 전체 스루풋을 상승시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;2219&quot;&gt;&lt;b data-start-index=&quot;2219&quot;&gt;  &lt;span style=&quot;color: #333333;&quot;&gt;[요약] 병목 지점 관점에서의 성능 개선 방법 정리&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;2294&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b data-start-index=&quot;2294&quot;&gt;1. EC2 (웹 애플리케이션) 병목 발생 시&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 내 비효율적인 로직(ex. 무한 루프 등 CPU 낭비 코드)이 있는지 체크하고 개선하기&lt;/li&gt;
&lt;li&gt;정적 파일(S3) 서빙이 분리되어 있는지 확인하고 분리하기&lt;/li&gt;
&lt;li&gt;수평적 확장 (서버 개수 늘리기 + 로드 밸런서)&lt;/li&gt;
&lt;li&gt;수직적 확장 (서버 스펙 올리기)&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;color: #c7c7c7;&quot; data-start-index=&quot;2446&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b data-start-index=&quot;2446&quot;&gt;2. DB (데이터베이스) 병목 발생 시&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(최우선) 비효율적인 쿼리 개선 (인덱스 적용, SQL 튜닝, 역정규화 등)&lt;/li&gt;
&lt;li&gt;수직적 확장 (비용 고려 필요)&lt;/li&gt;
&lt;li&gt;읽기 전용 데이터베이스(Read Replica) 도입&lt;/li&gt;
&lt;li&gt;캐시 서버(Cache Server) 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. youtube : &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;[인프런] 대규모 트래픽 처리를 위한 부하 테스트 입문/실전&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/playlist?list=PLtUgHNmvcs6qAqWz-UhH-_ploSbK2eHwG&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/playlist?list=PLtUgHNmvcs6qAqWz-UhH-_ploSbK2eHwG&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783090723747&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[인프런] 대규모 트래픽 처리를 위한 부하 테스트 입문/실전&quot; data-og-description=&quot;풀버전 강의는 아래 링크를 참고해주세요 ✔ https://inf.run/f23VE&quot; data-og-host=&quot;www.youtube.com&quot; data-og-source-url=&quot;https://www.youtube.com/playlist?list=PLtUgHNmvcs6qAqWz-UhH-_ploSbK2eHwG&quot; data-og-url=&quot;http://www.youtube.com/playlist?list=PLtUgHNmvcs6qAqWz-UhH-_ploSbK2eHwG&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4a1wF/dJMb9efo6Mg/nCyKsFjZko9lPFC1KDbKkK/img.jpg?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/hhNXp/dJMb9lMmxAm/60CwaT5a5iZt5AOl38nYG0/img.jpg?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/bXTgmr/dJMb9hDceSV/YYFPHBweXu3H0PxQROk0Xk/img.jpg?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270&quot;&gt;&lt;a href=&quot;https://www.youtube.com/playlist?list=PLtUgHNmvcs6qAqWz-UhH-_ploSbK2eHwG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.youtube.com/playlist?list=PLtUgHNmvcs6qAqWz-UhH-_ploSbK2eHwG&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4a1wF/dJMb9efo6Mg/nCyKsFjZko9lPFC1KDbKkK/img.jpg?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/hhNXp/dJMb9lMmxAm/60CwaT5a5iZt5AOl38nYG0/img.jpg?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/bXTgmr/dJMb9hDceSV/YYFPHBweXu3H0PxQROk0Xk/img.jpg?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[인프런] 대규모 트래픽 처리를 위한 부하 테스트 입문/실전&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;풀버전 강의는 아래 링크를 참고해주세요 ✔ https://inf.run/f23VE&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.youtube.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 블로그 : Grafana&amp;nbsp;k6&amp;nbsp;-&amp;nbsp;터미널에서&amp;nbsp;5분만에&amp;nbsp;도입하는&amp;nbsp;부하테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://innovation123.tistory.com/295&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://innovation123.tistory.com/295&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783091751160&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Grafana k6 - 터미널에서 5분만에 도입하는 부하테스트&quot; data-og-description=&quot;이 글에서는 터미널에서 서버 성능 테스트를 아주 간편하게 할 수 있는 방안을 소개한다.작성자는 &amp;quot;k6 부하테스트&amp;quot;라는 단어를 처음 보고 나서 5분 내에 실제 부하테스트를 완료하고, 결과를 도&quot; data-og-host=&quot;innovation123.tistory.com&quot; data-og-source-url=&quot;https://innovation123.tistory.com/295&quot; data-og-url=&quot;https://innovation123.tistory.com/295&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lWa8W/dJMb8YXWW7s/sMc91t6aJTu9FzBqt4TI81/img.png?width=564&amp;amp;height=476&amp;amp;face=0_0_564_476,https://scrap.kakaocdn.net/dn/bgHDEJ/dJMb8QMnnS3/pt3FXnByti7yRJkSliGZQk/img.png?width=564&amp;amp;height=476&amp;amp;face=0_0_564_476,https://scrap.kakaocdn.net/dn/QYYrE/dJMb8Qexp1C/VKfAtu5ie2ick4yS1uKRfk/img.png?width=2972&amp;amp;height=1504&amp;amp;face=0_0_2972_1504&quot;&gt;&lt;a href=&quot;https://innovation123.tistory.com/295&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://innovation123.tistory.com/295&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lWa8W/dJMb8YXWW7s/sMc91t6aJTu9FzBqt4TI81/img.png?width=564&amp;amp;height=476&amp;amp;face=0_0_564_476,https://scrap.kakaocdn.net/dn/bgHDEJ/dJMb8QMnnS3/pt3FXnByti7yRJkSliGZQk/img.png?width=564&amp;amp;height=476&amp;amp;face=0_0_564_476,https://scrap.kakaocdn.net/dn/QYYrE/dJMb8Qexp1C/VKfAtu5ie2ick4yS1uKRfk/img.png?width=2972&amp;amp;height=1504&amp;amp;face=0_0_2972_1504');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Grafana k6 - 터미널에서 5분만에 도입하는 부하테스트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 터미널에서 서버 성능 테스트를 아주 간편하게 할 수 있는 방안을 소개한다.작성자는 &quot;k6 부하테스트&quot;라는 단어를 처음 보고 나서 5분 내에 실제 부하테스트를 완료하고, 결과를 도&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;innovation123.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/대규모 트래픽 처리</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/568</guid>
      <comments>https://annovation.tistory.com/568#entry568comment</comments>
      <pubDate>Fri, 3 Jul 2026 23:49:07 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] Redisson은 어떻게 Redis Pub/Sub을 이용해 재시도(Retry) 로직을 구현할 수 있을까?</title>
      <link>https://annovation.tistory.com/563</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Redis와 Redisson&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리 기반 Key-Value NoSQL 데이터베이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 메모리에 저장하기 때문에 매우 빠른 읽기/쓰기 성능을 제공하며 캐시, 세션 저장소, Pub/Sub, 분산 락 등에 자주 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson은 Java 환경에서 Redis를 쉽게 사용할 수 있도록 제공하는 Client 라이브러리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client 라이브러리란 애플리케이션과 외부 시스템 사이의 통신을 추상화하여 개발자가 쉽게 사용할 수 있도록 도와주는 라이브러리를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 개발자는&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;lock.lock();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 호출하지만 실제로는 Redisson이 Redis 명령어를 생성하여 Redis 서버와 통신한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Java Application
       &amp;darr;
    Redisson
       &amp;darr;
 Redis Command
       &amp;darr;
     Redis&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 공식 문서&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/&quot;&gt;https://redis.io/docs/latest/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redisson 공식 문서&lt;br /&gt;&lt;a href=&quot;https://redisson.pro/docs/&quot;&gt;https://redisson.pro/docs/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Redis 분산 락과 SET NX&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 분산 락은 일반적으로 SET NX 명령을 이용하여 구현된다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;SET lock:product:1 serverA NX&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NX(Not Exists)는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;해당 Key가 존재하지 않을 때만 저장&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 서버와 B 서버가 동시에 락 획득을 시도하면&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;A 서버 &amp;rarr; 성공

B 서버 &amp;rarr; 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 A가 Key를 생성했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Key 생성 성공
=
락 획득 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 이해할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis SET Command&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/set/&quot;&gt;https://redis.io/docs/latest/commands/set/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;EX(PX)를 사용하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 TTL 없이 락을 생성했다면&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;SET lock:product:1 serverA NX&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 서버가 장애로 종료되었을 때 락이 영구적으로 남을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;A 서버 락 획득

&amp;darr;

서버 장애

&amp;darr;

unlock() 실패

&amp;darr;

락 영구 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 TTL을 설정한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SET lock:product:1 serverA NX EX 30&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EX는 초(second),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PX는 밀리초(milliseconds) 단위 TTL을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL이 지나면 Redis가 자동으로 락을 제거한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis SET Command (EX, PX 옵션 포함)&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/set/&quot;&gt;https://redis.io/docs/latest/commands/set/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Redisson Watchdog&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL만 설정하면 또 다른 문제가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;TTL : 30초

실제 작업 시간 : 60초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 끝나기 전에 TTL이 만료되면 다른 서버가 락을 획득할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 Redisson은 Watchdog 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Watchdog은 락을 보유한 스레드가 살아있는 동안 TTL을 자동으로 연장한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;작업 중

&amp;darr;

TTL 연장

&amp;darr;

작업 중

&amp;darr;

TTL 연장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 작업 시간이 예상보다 길어져도 락이 중간에 해제되지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson Locks and Synchronizers&lt;br /&gt;&lt;a href=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&quot;&gt;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Polling이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Polling은 특정 자원의 상태를 주기적으로 확인하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문 열렸어?

&amp;darr;

아직

&amp;darr;

문 열렸어?

&amp;darr;

아직&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 계속 확인하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Resource는 상태를 확인하려는 대상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 분산 락에서는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;lock:123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 락 자체가 Resource가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MDN Polling&lt;br /&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Polling&quot;&gt;https://developer.mozilla.org/en-US/docs/Glossary/Polling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Redis를 계속 조회한다는 의미&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Polling 방식에서는 락 획득에 실패하면 반복적으로 Redis에 명령을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while(true){

    tryLock();

    Thread.sleep(100);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 비슷한 형태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;SET lock:123 NX&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령을 계속 보내면서 락 획득을 시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;Redis를 계속 조회한다&amp;rdquo;는 말은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 반복적으로 명령을 보내 락 상태를 확인하거나 락 획득을 시도한다는 의미이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis SET Command&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/set/&quot;&gt;https://redis.io/docs/latest/commands/set/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Redis Pub/Sub&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Publish / Subscribe 메시징 모델을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성 요소는 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Publisher&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 발행하는 주체&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Subscriber&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 구독하는 주체&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Channel&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지가 전달되는 통로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscriber는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SUBSCRIBE lock:123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령을 통해 채널을 구독한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Publisher가&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;PUBLISH lock:123 unlock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령을 실행하면 Redis는 해당 채널을 구독하고 있는 클라이언트에게 메시지를 전달한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis Pub/Sub&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/interact/pubsub/&quot;&gt;https://redis.io/docs/latest/develop/interact/pubsub/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis SUBSCRIBE Command&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/subscribe/&quot;&gt;https://redis.io/docs/latest/commands/subscribe/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis PUBLISH Command&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/publish/&quot;&gt;https://redis.io/docs/latest/commands/publish/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;락 채널을 구독한다는 의미&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson은 락 획득에 실패하면 Polling 방식으로 Redis를 계속 조회하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 해당 락과 연결된 Pub/Sub 채널을 구독한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;락 획득 실패

&amp;darr;

&quot;락이 해제되면 알려줘&quot;

&amp;darr;

대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 &amp;ldquo;락 채널을 구독한다&amp;rdquo;는 의미이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson Locks and Synchronizers&lt;br /&gt;&lt;a href=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&quot;&gt;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Publish는 누가 수행하는가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람들이 Redis가 Publish를 수행한다고 생각하지만 정확히는 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;lock.unlock();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;darr;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;darr;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;PUBLISH lock-channel unlock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;darr;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;darr;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자에게 메시지 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Publish 명령 실행
=
Redisson

메시지 전달
=
Redis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis PUBLISH Command&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/publish/&quot;&gt;https://redis.io/docs/latest/commands/publish/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redisson Locks and Synchronizers&lt;br /&gt;&lt;a href=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&quot;&gt;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Redisson이 재시도 로직을 구현할 수 있는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson은 락 획득에 실패하면 Polling 방식으로 Redis를 계속 조회하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 Pub/Sub 채널을 구독하고 대기한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 락을 보유한 클라이언트가 unlock()을 호출하면 Redisson이 Publish를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 해당 메시지를 구독 중인 클라이언트에게 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 받은 클라이언트는 다시 SET NX 기반의 락 획득을 시도한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;락 획득 실패

&amp;darr;

Subscribe

&amp;darr;

대기

&amp;darr;

unlock()

&amp;darr;

Publish

&amp;darr;

메시지 수신

&amp;darr;

SET NX 재시도

&amp;darr;

락 획득 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Redisson은 Redis Pub/Sub을 이용하여 Polling 없이 효율적인 재시도 구조를 구현할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson Locks and Synchronizers&lt;br /&gt;&lt;a href=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&quot;&gt;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis Pub/Sub&lt;br /&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/interact/pubsub/&quot;&gt;https://redis.io/docs/latest/develop/interact/pubsub/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/563</guid>
      <comments>https://annovation.tistory.com/563#entry563comment</comments>
      <pubDate>Wed, 24 Jun 2026 23:36:28 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] Redisson 분산 락 적용 중 트랜잭션 커밋 순서 문제를 해결한 과정 (2)</title>
      <link>https://annovation.tistory.com/562</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;추가 해결 사항&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson 분산 락 적용 중 트랜잭션 커밋 순서 문제를 해결하는 과정에서 다수의 동시 요청이 하나의 재고 락에 몰리면서 일부 요청이 &lt;b&gt;tryLock&lt;/b&gt;의 대기 시간 안에 락을 획득하지 못하는 상황이 발생했고, 이 요청들이 락 획득 실패 예외로 처리되어 동시성 테스트 통과가 이뤄지지 않는 문제가 발생했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 증상&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 &lt;b&gt;tryLock(WAIT_TIME_SECONDS, LEASE_TIME_SECONDS, TimeUnit.SECONDS)&lt;/b&gt;에서&amp;nbsp; &lt;b&gt;WAIT_TIME_SECONDS&lt;/b&gt; 동안 락 획득을 기다리지만, 그 시간 안에 락을 얻지 못하면 &lt;b&gt;false&lt;/b&gt;를 반환한다.&lt;/li&gt;
&lt;li&gt;이를 곧바로 &lt;b&gt;STOCK_LOCK_TIMEOUT&lt;/b&gt; 예외로 변환했기 때문에 일부 요청이 재시도 없이 실패 처리되어 동시성 테스트가 통과하지 못했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780990267046&quot; class=&quot;aspectj&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.hubEleven.stock.infrastructure.lock;

@Component
@RequiredArgsConstructor
public class RedissonStockLockManager implements StockLockManager {

	private static final long WAIT_TIME_SECONDS = 3L;
	private static final long LEASE_TIME_SECONDS = 5L;

	private final RedissonClient redissonClient;

	@Override
	public &amp;lt;T&amp;gt; T executeWithLock(String lockKey, Supplier&amp;lt;T&amp;gt; supplier) {
		RLock lock = redissonClient.getLock(lockKey);
		boolean locked = false;

		try {
			locked = lock.tryLock(WAIT_TIME_SECONDS, LEASE_TIME_SECONDS, TimeUnit.SECONDS);
			if (!locked) {
				throw new GlobalException(STOCK_LOCK_TIMEOUT);
			}

			return supplier.get();
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new GlobalException(STOCK_LOCK_TIMEOUT);
		} finally {
			if (locked &amp;amp;&amp;amp; lock.isHeldByCurrentThread()) {
				lock.unlock();
			}
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;&lt;span&gt;해결 방안 검토&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대략적으로 해결 가능한 락 획득 대기 시간 초과 시 예외 처리 정책을 요약해보면 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%; text-align: center;&quot;&gt;정책&lt;/td&gt;
&lt;td style=&quot;width: 15.1162%; text-align: center;&quot;&gt;요약&lt;/td&gt;
&lt;td style=&quot;width: 41.9187%; text-align: center;&quot;&gt;세부 내용&lt;/td&gt;
&lt;td style=&quot;width: 12.1801%; text-align: center;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 15.6686%; text-align: center;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%; text-align: center;&quot;&gt;A&lt;/td&gt;
&lt;td style=&quot;width: 15.1162%; text-align: center;&quot;&gt;너무 오래 기다리는 요청은 실패시킨다&lt;/td&gt;
&lt;td style=&quot;width: 41.9187%; text-align: left;&quot;&gt;- 트래픽 폭주 시 일부 요청은 빠르게 실패시키고 재시도를 유도한다.&lt;br /&gt;- WAIT_TIME을 제한적으로 설정&lt;br /&gt;- 락 획득 실패 시 409 CONFLICT 반환&lt;br /&gt;- 클라이언트가 재시도하거나 사용자에게 안내&lt;/td&gt;
&lt;td style=&quot;width: 12.1801%; text-align: left;&quot;&gt;- 서버가&amp;nbsp;오래&amp;nbsp;붙잡히지&amp;nbsp;않음&lt;br /&gt;- 트래픽&amp;nbsp;폭주&amp;nbsp;시&amp;nbsp;빠르게&amp;nbsp;실패&amp;nbsp;처리&amp;nbsp;가능&lt;/td&gt;
&lt;td style=&quot;width: 15.6686%; text-align: left;&quot;&gt;- 사용자 요청 일부가 실패할 수 있음&lt;br /&gt;- 클라이언트 재시도 설계가 필요함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%; text-align: center;&quot;&gt;B&lt;/td&gt;
&lt;td style=&quot;width: 15.1162%; text-align: center;&quot;&gt;서버 내부에서 재시도 처리&lt;/td&gt;
&lt;td style=&quot;width: 41.9187%; text-align: left;&quot;&gt;1. 락 획득 시도&lt;br /&gt;2. 실패&lt;br /&gt;3. 짧게 대기&lt;br /&gt;4. 다시 시도&lt;br /&gt;5. 몇 회 실패하면 최종 실패&lt;/td&gt;
&lt;td style=&quot;width: 12.1801%; text-align: left;&quot;&gt;- 순간적인 경합은 흡수 가능&lt;br /&gt;- 사용자 실패율 감소&lt;/td&gt;
&lt;td style=&quot;width: 15.6686%; text-align: left;&quot;&gt;- 응답 시간이 길어질 수 있음&lt;br /&gt;- 재시도 폭주가 생기면 서버 부하 증가&lt;br /&gt;- 재시도 횟수/간격 정책이 필요함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%; text-align: center;&quot;&gt;C&lt;/td&gt;
&lt;td style=&quot;width: 15.1162%; text-align: center;&quot;&gt;주문 요청을 큐로 보내 순차 처리&lt;/td&gt;
&lt;td style=&quot;width: 41.9187%; text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;- Kafka, RabbitMQ 같은 큐를 사용&lt;br /&gt;- 주문 요청&lt;br /&gt;&amp;nbsp;&amp;rarr; 큐에 적재&lt;br /&gt;&amp;nbsp;&amp;rarr; 재고 감소 consumer가 순서대로 처리&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.1801%; text-align: left;&quot;&gt;- 대량 트래픽을 안정적으로 흡수 가능&lt;br /&gt;- 재고 감소를 순차 처리하기 쉬움&lt;br /&gt;- 락 경합 감소&lt;/td&gt;
&lt;td style=&quot;width: 15.6686%; text-align: left;&quot;&gt;- 구현 복잡도 증가&lt;br /&gt;- 비동기 처리 구조 필요&lt;br /&gt;- 사용자에게 즉시 확정 응답을 주기 어려울 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : 락&amp;nbsp;획득&amp;nbsp;실패&amp;nbsp;시&amp;nbsp;제한적&amp;nbsp;재시도&amp;nbsp;후&amp;nbsp;명시적인&amp;nbsp;예외를&amp;nbsp;반환&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1781618988864&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;최대 3번 반복:
  tryLock 시도
  성공하면 supplier 실행
  실패하면 짧게 대기 후 재시도

3번 모두 실패:
  STOCK_LOCK_TIMEOUT 예외&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;락 획득 실패를 곧바로 비즈니스 실패로 처리하면 순간적인 트래픽 집중 상황에서 정상 처리 가능한 요청까지 실패할 수 있다고 판단했다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;이에 제한된 횟수의 재시도를 통해 일시적인 락 경합을 완화하고, 재시도 후에도 락을 획득하지 못한 경우에는 &lt;b&gt;STOCK_LOCK_TIMEOUT&lt;/b&gt; 예외를 명시적으로 반환하여 클라이언트 또는 상위 계층에서 재시도 여부를 판단할 수 있도록 설계했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Redisson 재시도 구조가 가능한 이유&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Redis Pub/Sub 이란?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 자료 :&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson은 락 획득 실패 시 Redis를 계속 조회하는 Polling 방식을 사용하지 않고 Pub/Sub를 사용한다.&lt;/li&gt;
&lt;li&gt;락 해제 시 Publish를 수행하고 대기 중인 클라이언트는 Subscribe 상태에서 알림을 받는다.&lt;/li&gt;
&lt;li&gt;알림을 받은 클라이언트는 다시 락 획득을 시도한다. (재시도)&lt;/li&gt;
&lt;li&gt;이를 통해 Redis 조회 횟수를 줄이고 효율적으로 재시도할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Redis Pub/Sub 구조&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782282870877&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;락 획득 실패

&quot;락이 풀리면 알려줘&quot;

기다림
&amp;darr;
락 해제
&amp;darr;
메시지 수신
&amp;darr;
재시도&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;검증&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start; background-color: #f6e199;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;동시성&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;테스트&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 테스트 진행 방식&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 100개의 요청이 같은 상품 재고를 1개씩 감소시키는 테스트를 작성했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 테스트 통과 결과&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson 분산 락과 트랜잭션 경계 분리 후 테스트가 통과했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-16 at 11.43.44 PM.png&quot; data-origin-width=&quot;2716&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GSFj8/dJMcai4HOCY/Lono3XfOk2mBRC17Xdfo01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GSFj8/dJMcai4HOCY/Lono3XfOk2mBRC17Xdfo01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GSFj8/dJMcai4HOCY/Lono3XfOk2mBRC17Xdfo01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGSFj8%2FdJMcai4HOCY%2FLono3XfOk2mBRC17Xdfo01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2716&quot; height=&quot;1536&quot; data-filename=&quot;Screenshot 2026-06-16 at 11.43.44 PM.png&quot; data-origin-width=&quot;2716&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;개선할 사항&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추후 확작성을 고려하다면, 모든 요청이 순차적으로 처리될 수 있게 Kafka, RabbitMQ 같은 기술을 활용해 재시도 로직을 개선하면 좋을 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;참고 자료&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/562</guid>
      <comments>https://annovation.tistory.com/562#entry562comment</comments>
      <pubDate>Tue, 23 Jun 2026 23:00:56 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] Redisson 분산 락 적용 중 트랜잭션 커밋 순서 문제를 해결한 과정 (1)</title>
      <link>https://annovation.tistory.com/561</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;진행 환경&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;진행 환경&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Java 17&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Spring Boot 3.5.7&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;MySQL 8.0.44&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Gradle&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-06-08 at 1.50.30 PM.png&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;1608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z0uZP/dJMcagyZ861/x6qMVmgoLxP9Pc6jRwCfZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z0uZP/dJMcagyZ861/x6qMVmgoLxP9Pc6jRwCfZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z0uZP/dJMcagyZ861/x6qMVmgoLxP9Pc6jRwCfZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ0uZP%2FdJMcagyZ861%2Fx6qMVmgoLxP9Pc6jRwCfZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1150&quot; height=&quot;643&quot; data-filename=&quot;Screenshot 2026-06-08 at 1.50.30 PM.png&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;1608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199; color: #222222; text-align: start;&quot;&gt;문제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;증상&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Redisson 분산락을 적용했지만, 100개의 동시 요청을 보냈을 때 재고가 기대값처럼 정확히 차감되지 않았다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;테스트 기대값은 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780895346237&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;초기 재고 : 100
요청 수 : 100개
요청당 차감 수량 : 1
기대 결과 : 최종 재고 0
실제 결과 : 최종 재고가 0이 아니거나, 일부 요청이 실패하는 동시성 문제가 발생&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : 락 해제가 트랜잭션 커밋보다 먼저 일어난다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗️StockServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1780891203860&quot; class=&quot;coffeescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public StockResult decreaseStock(StockRequests.Decrease request) {
    return stockLockManager.executeWithLock(
            &quot;stock:decrease:&quot; + request.productId(),
            () -&amp;gt; {
                Product product = getProductOrThrow(request.productId());
                Stock stock = getStockOrThrow(request.productId());
                stock.decreaseQuantity(request.quantity());
                return StockResult.from(stock, product.getName());
            }
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;현재 구조는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;StockServiceImpl.decreaseStock()&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;에&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;@Transactional&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이 붙어 있고, 그 메서드 내부에서 Redisson 락을 획득한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;❗️Redisson 분산락 적용한 재고 감소 서비스 로직 흐름&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780895492809&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;트랜잭션 시작
&amp;rarr; Redisson 락 획득
&amp;rarr; 재고 조회
&amp;rarr; 재고 차감
&amp;rarr; 락 해제
&amp;rarr; 트랜잭션 커밋&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Spring의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;@Transactional&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;은 프록시가 메서드 호출 전후로 트랜잭션을 관리한다. 따라서 메서드 내부의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;finally&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;에서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;unlock()&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이 실행된 뒤, 메서드가 완전히 반환되고 나서야 트랜잭션 커밋이 일어난다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;락이 풀렸지만 이전 요청의 재고 감소가 아직 DB에 commit되지 않은 상태라면, 다음 요청이 lock을 획득하고 확정되지 않은 데이터를 기준으로 재고를 조회할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;분산 락의 목적은 같은 상품 재고 감소를 순차 처리하는 것인데, commit 이전에 lock이 풀리면 정합성 보장이 약해진다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗️잘못된 흐름 예시&lt;/p&gt;
&lt;pre id=&quot;code_1780895651010&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Thread A: 재고 100 조회
Thread A: 재고 99로 변경
Thread A: 락 해제
Thread B: 락 획득
Thread B: 아직 A 커밋 전이라 DB에서 재고 100 조회
Thread A: 커밋
Thread B: 재고 99로 변경
Thread B: 커밋&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방안 검토&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 1 :&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : lock을 잡는 책임과 실제 DB 변경 트랜잭션을 분리&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockServiceImpl은 lock 획득/해제 흐름을 담당하고,&amp;nbsp;StockDecreaseProcessor는&amp;nbsp;@Transactional로 실제 재고 감소를 담당하게 했다.&lt;/li&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780891203861&quot; class=&quot;coffeescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public StockResult decreaseStock(Decrease request) {
    return stockLockManager.executeWithLock(
            &quot;stock:decrease:&quot; + request.productId(),
            () -&amp;gt; stockDecreaseProcessor.decrease(request)
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780891203861&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public StockResult decrease(Decrease request) {
    Stock stock = stockRepository.findByProductId(request.productId())
            .orElseThrow(...);

    stock.decreaseQuantity(request.quantity());

    return StockResult.from(stock, productName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;트랜잭션 분리 ?&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;트랜잭션을 분리하는 이유&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson&amp;nbsp;분산&amp;nbsp;락을&amp;nbsp;적용할&amp;nbsp;때&amp;nbsp;단순히&amp;nbsp;`@Transactional`&amp;nbsp;메서드&amp;nbsp;내부에서&amp;nbsp;lock/unlock을&amp;nbsp;처리하면,&amp;nbsp;`finally`에서&amp;nbsp;lock이&amp;nbsp;해제된&amp;nbsp;뒤&amp;nbsp;Spring&amp;nbsp;트랜잭션&amp;nbsp;commit이&amp;nbsp;수행될&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;경우&amp;nbsp;다음&amp;nbsp;요청이&amp;nbsp;lock을&amp;nbsp;획득했지만&amp;nbsp;이전&amp;nbsp;요청의&amp;nbsp;DB&amp;nbsp;변경은&amp;nbsp;아직&amp;nbsp;commit되지&amp;nbsp;않은&amp;nbsp;상태가&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있어,&amp;nbsp;재고&amp;nbsp;정합성&amp;nbsp;보장이&amp;nbsp;약해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를&amp;nbsp;방지하기&amp;nbsp;위해&amp;nbsp;lock을&amp;nbsp;잡는&amp;nbsp;바깥&amp;nbsp;서비스와&amp;nbsp;실제&amp;nbsp;DB&amp;nbsp;변경을&amp;nbsp;수행하는&amp;nbsp;`@Transactional`&amp;nbsp;Processor를&amp;nbsp;분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로&amp;nbsp;실행&amp;nbsp;순서를&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;보장했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782100856733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;`lock 획득 &amp;rarr; 트랜잭션 시작 &amp;rarr; 재고 감소 &amp;rarr; commit &amp;rarr; lock 해제`&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 분리하지 않은 구조&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2528&quot; data-origin-height=&quot;1422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOwoGJ/dJMcadvBxp9/FrMMjdpC8pmjuUDx8EKlc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOwoGJ/dJMcadvBxp9/FrMMjdpC8pmjuUDx8EKlc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOwoGJ/dJMcadvBxp9/FrMMjdpC8pmjuUDx8EKlc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOwoGJ%2FdJMcadvBxp9%2FFrMMjdpC8pmjuUDx8EKlc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2528&quot; height=&quot;1422&quot; data-origin-width=&quot;2528&quot; data-origin-height=&quot;1422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 문제 흐름&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락은 풀렸는데 DB 변경은 아직 커밋되지 않은 상태가 생길 수 있다.&lt;/li&gt;
&lt;li&gt;그러면&amp;nbsp;다음&amp;nbsp;요청이&amp;nbsp;확정되지&amp;nbsp;않은&amp;nbsp;데이터를&amp;nbsp;기준으로&amp;nbsp;동작할&amp;nbsp;위험이&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 분리한 구조&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3234&quot; data-origin-height=&quot;1614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfz9JI/dJMcaaMvfMM/JA0JXt1PJG22OmjE2Uo05k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfz9JI/dJMcaaMvfMM/JA0JXt1PJG22OmjE2Uo05k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfz9JI/dJMcaaMvfMM/JA0JXt1PJG22OmjE2Uo05k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfz9JI%2FdJMcaaMvfMM%2FJA0JXt1PJG22OmjE2Uo05k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3234&quot; height=&quot;1614&quot; data-origin-width=&quot;3234&quot; data-origin-height=&quot;1614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 요청이 여러 개일 때&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 요청은 이전 요청의 DB commit이 끝난 뒤 lock을 획득한다.&lt;/li&gt;
&lt;li&gt;따라서&amp;nbsp;확정된&amp;nbsp;재고&amp;nbsp;값을&amp;nbsp;기준으로&amp;nbsp;감소&amp;nbsp;로직을&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프록시가 뭔지&lt;/li&gt;
&lt;li&gt;프록시 에서 커밋하는 로직&lt;/li&gt;
&lt;li&gt;트랜잭션이 뭔지&lt;/li&gt;
&lt;li&gt;트랜잭션 분리하는 이유&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/561</guid>
      <comments>https://annovation.tistory.com/561#entry561comment</comments>
      <pubDate>Mon, 22 Jun 2026 14:27:23 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 재고 감소 락 추상화 및 Redisson 락 구현 (아키텍처 구조 보완 필요)</title>
      <link>https://annovation.tistory.com/557</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;아키텍처 구조&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;현재 프로젝트 구조&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;인터페이스 구현 구조&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780406595077&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;application service
    &amp;darr;
application port(interface)
    &amp;uarr;
infrastructure adapter(implementation)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson에 대한 직접 의존을 application service에서 분리하기 위해, application 계층에 port 인터페이스를 두고 infrastructure 계층에서 구현하도록 설계했다.&lt;/li&gt;
&lt;li&gt;(&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;왜 인터페이스는 application 계층에 두고, 구현체는 infrastructure 에 두는 걸까?)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;아 어렵넹&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Lock 추상화 인터페이스 StockLockManager 구현&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;StockLockManager.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780376575426&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.hubEleven.stock.application.port;

import java.util.function.Supplier;

public interface StockLockManager {

	&amp;lt;T&amp;gt; T executeWithLock(String lockKey, Supplier&amp;lt;T&amp;gt; supplier);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: color(srgb 1 1 1 / 0.107059);&quot;&gt;lockKey &lt;/span&gt;: 어떤 작업에 락을 걸지 구분하는 키&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: color(srgb 1 1 1 / 0.107059);&quot;&gt;Supplier&amp;lt;T&amp;gt; &lt;/span&gt;: 락을 잡은 뒤 실행할 실제 작업&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: color(srgb 1 1 1 / 0.107059);&quot;&gt;&amp;lt;T&amp;gt; &lt;/span&gt;: 작업 결과 타입을 유연하게 받기 위한 제네릭&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;코드 작동 예시&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780376708882&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;이 lockKey로 락을 잡고,
락을 잡은 동안 supplier 안의 재고 감소 작업을 실행해줘&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;Redisson&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;StockLockManager.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780407480981&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.hubEleven.stock.infrastructure.lock;

import static com.hubEleven.stock.domain.exception.StockErrorCode.STOCK_LOCK_TIMEOUT;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import com.commonLib.common.exception.GlobalException;
import com.hubEleven.stock.application.port.StockLockManager;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class RedissonStockLockManager implements StockLockManager {

	private static final long WAIT_TIME_SECONDS = 3L;
	private static final long LEASE_TIME_SECONDS = 5L;

	private final RedissonClient redissonClient;

	@Override
	public &amp;lt;T&amp;gt; T executeWithLock(String lockKey, Supplier&amp;lt;T&amp;gt; supplier) {
		RLock lock = redissonClient.getLock(lockKey);
		boolean locked = false;

		try {
			locked = lock.tryLock(WAIT_TIME_SECONDS, LEASE_TIME_SECONDS, TimeUnit.SECONDS);
			if (!locked) {
				throw new GlobalException(STOCK_LOCK_TIMEOUT);
			}

			return supplier.get();
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new GlobalException(STOCK_LOCK_TIMEOUT);
		} finally {
			if (locked &amp;amp;&amp;amp; lock.isHeldByCurrentThread()) {
				lock.unlock();
			}
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 하나 씩 살펴보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Redis에서 lockKey 이름의 자물쇠를 가져온다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780408391659&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RLock lock = redissonClient.getLock(lockKey);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 lockKey 는 서비스 코드에서 생성되며, 재고 감소하려는 상품 ID (productId) 를 조합하여 생성한다.&lt;/li&gt;
&lt;li&gt;tryLock 시도 후 이 lockKey 가 겹쳤을 때, 동시성 제어를 하게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 최대 3초 동안 자물쇠를 잡으려고 기다리고, 잡으면 5초 동안 락이 유지된다.&lt;/p&gt;
&lt;pre id=&quot;code_1780408451705&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;locked = lock.tryLock(3, 5, TimeUnit.SECONDS);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 락을 획득한 뒤, 실제 작업을 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1780408642588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return supplier.get();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 작업 성공, 실패 여부와 상관 없이 마지막에는 락을 해체한다.&lt;/p&gt;
&lt;pre id=&quot;code_1780408688326&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;finally {
    if (locked &amp;amp;&amp;amp; lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;Redisson&amp;nbsp;분산&amp;nbsp;락&amp;nbsp;기반&amp;nbsp;재고&amp;nbsp;감소&amp;nbsp;동시성&amp;nbsp;제어&amp;nbsp;흐름&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;1398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mrFlJ/dJMcaiwMpaq/hqAnZLO6MuYAT0kTy7m1Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mrFlJ/dJMcaiwMpaq/hqAnZLO6MuYAT0kTy7m1Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mrFlJ/dJMcaiwMpaq/hqAnZLO6MuYAT0kTy7m1Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmrFlJ%2FdJMcaiwMpaq%2FhqAnZLO6MuYAT0kTy7m1Dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1816&quot; height=&quot;1398&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;1398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;RLock 이 뭐지?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;그래서 supplier.get() 은 무슨 작업을 하는거지?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 구현하고자 하는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782796066049&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 락을 잡는다
2. 재고 차감 코드를 실행한다
3. 락을 푼다&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Supplier 에게 부여한 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782796095865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;이 재고 차감 코드를 지금 실행하지 말고,
나중에 내가 get()이라고 부르면 그때 실행해.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Supplier 로 구현한 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782796118897&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;new Supplier&amp;lt;StockResult&amp;gt;() {
    @Override
    public StockResult get() {
        return stockDecreaseProcessor.decrease(request);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782796143531&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;get()이라는 메서드를 가진 객체를 하나 만든다.
그 get() 안에는 재고 차감 코드가 들어 있다.
하지만 아직 get()을 호출하지 않았으니까 재고 차감은 실행되지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;supplier.get() 의 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782796244947&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;supplier.get();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 이 줄을 만나면 Java가 get() 안으로 들어가 아래 코드를 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1782796271552&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public StockResult get() {
    return stockDecreaseProcessor.decrease(request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reddison Lock 코드 흐름&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782796345413&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. lock.tryLock()으로 락 잡기
2. locked가 true인지 확인
3. supplier.get() 호출
4. get() 안에 있던 stockDecreaseProcessor.decrease(request) 실행
5. finally에서 unlock()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결론&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782796360132&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Supplier는 재고 차감 코드를 바로 실행하지 않게 감싸놓은 객체다.
supplier.get()은 감싸놓은 재고 차감 코드를 실제로 실행하는 호출이다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Questions&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;1️⃣ 왜 &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;StockServiceImpl&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;나&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RLock&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;을 직접 쓰지 않고,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;StockLockManager&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt; 같은 인터페이스를 거치게 만드는 게 좋을까?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;StockServiceImpl&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;나&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RLock&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;을 직접 쓰면 서비스 코드가 Redisson과의 의존성이 강해진다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;예를 들어 서비스 코드에 아래와 같이 RLock 을 직접 사용하게 되면,&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780375900898&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StockServiceImpl {

    private final RedissonClient redissonClient;

    public void decrease() {
        RLock lock = redissonClient.getLock(&quot;stock&quot;);

        lock.lock();

        try {
            // 재고 감소
        } finally {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락 방식을 변경하고자 할 때, 서비스 코드를 직접 수정해야한다.&lt;/li&gt;
&lt;li&gt;즉, 하위 기술 변경이 상위 비즈니스 코드까지 연쇄적으로 수정되게 되고 유지보수가 어려워진다.&lt;/li&gt;
&lt;li&gt;따라서 StockLockManager 와 같은 락 추상화 인터페이스를 활용해 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780376179806&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StockLockManager {
    &amp;lt;T&amp;gt; T executeWithLock(String lockKey, Supplier&amp;lt;T&amp;gt; supplier);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스 구현체 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1780376334597&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StockLockManager
├─ RedissonStockLockManager
├─ DatabaseStockLockManager
└─ InMemoryStockLockManager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ 왜 executeWithLock(String lockKey, Supplier&amp;lt;T&amp;gt; supplier) 구조에서 &lt;u&gt;&lt;b&gt;락을 잡는 코드&lt;/b&gt;&lt;/u&gt;와 &lt;u&gt;&lt;b&gt;락 안에서 실행할 실제 작업&lt;/b&gt;&lt;/u&gt;을 분리해서 넘길까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Java Docs : Supplier&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1780409328498&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Supplier (Java Platform SE 8 )&quot; data-og-description=&quot;Represents a supplier of results. There is no requirement that a new or distinct result be returned each time the supplier is invoked. This is a functional interface whose functional method is get().&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Supplier (Java Platform SE 8 )&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Represents a supplier of results. There is no requirement that a new or distinct result be returned each time the supplier is invoked. This is a functional interface whose functional method is get().&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Redisson Docs : &lt;span style=&quot;color: #333333;&quot;&gt;Redisson Locks and Synchronizers&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a href=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/index.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1780409364176&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Locks and synchronizers - Redisson Reference Guide&quot; data-og-description=&quot;Locks and synchronizers Lock Valkey or Redis based distributed reentrant Lock object for Java and implements Lock interface. Uses pub/sub channel to notify other threads across all Redisson instances waiting to acquire a lock. If Redisson instance which ac&quot; data-og-host=&quot;redisson.pro&quot; data-og-source-url=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/index.html&quot; data-og-url=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/index.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redisson.pro/docs/data-and-services/locks-and-synchronizers/index.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Locks and synchronizers - Redisson Reference Guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Locks and synchronizers Lock Valkey or Redis based distributed reentrant Lock object for Java and implements Lock interface. Uses pub/sub channel to notify other threads across all Redisson instances waiting to acquire a lock. If Redisson instance which ac&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redisson.pro&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/557</guid>
      <comments>https://annovation.tistory.com/557#entry557comment</comments>
      <pubDate>Tue, 2 Jun 2026 23:00:47 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] RedissonClient Bean 설정 클래스 추가 (2) (리소스 보완 필요)</title>
      <link>https://annovation.tistory.com/556</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;RedissonClient 리소스&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient 리소스 란?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;리소스는 그 주소를 바탕으로 실제 통신을 처리하기 위해 내부에서 관리하는 것들을 말한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;공식 문서 기준으로 보면&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;는 Redisson 객체들에 접근하는 메인 인터페이스이다. (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.javadoc.io/static/org.redisson/redisson/3.42.0/org/redisson/api/RedissonClient.html&quot;&gt;RedissonClient Javadoc&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RLock&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RMap&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RAtomicLong&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 Redis 기반 객체를 가져오는 중심 진입점이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;어려웡&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;대표 리소스&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Redis 연결 풀&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Redisson 설정 문서에는 connection pool 관련 설정이 있다. (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://redisson.pro/docs/configuration/&quot;&gt;Redisson Configuration - connection pool settings&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779890416874&quot; class=&quot;ebnf&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;connectionMinimumIdleSize
connectionPoolSize
masterConnectionMinimumIdleSize
masterConnectionPoolSize
subscriptionConnectionPoolSize&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이건 Redisson이 Redis와 통신할 연결들을 풀로 관리한다는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779890416874&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;host/port = Redis 주소
connection pool = 그 주소로 실제 명령을 보내기 위해 준비해둔 연결 묶음&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Netty 스레드&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EventLoopGroup&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;종료해야 하는 클라이언트 인스턴스&lt;/b&gt;&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/556</guid>
      <comments>https://annovation.tistory.com/556#entry556comment</comments>
      <pubDate>Thu, 28 May 2026 23:00:50 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] RedissonClient Bean 설정 클래스 추가 (1)</title>
      <link>https://annovation.tistory.com/555</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;RedissonClient&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;RedissonClient 란?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779883555774&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Spring Boot 애플리케이션  ---&amp;gt;  Redis 서버&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Java 애플리케이션에서 Redis/Valkey를 사용할 수 있게 해주는 Redisson의 핵심 클라이언트 객체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;즉, Java 코드와 Redis &lt;span style=&quot;text-align: start;&quot;&gt;중간에서 통신을 도와주는 객체&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✅ Redisson 의 핵심 '클라이언트' 객체 ?&lt;br /&gt;요청을 받는 쪽 = 서버&lt;br /&gt;요청을 보내는 쪽 = 클라이언트&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;예시&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779883856812&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Redis 서버
&amp;rarr; 락 정보, 캐시 데이터 등을 저장하고 요청을 처리하는 쪽

Spring Boot 애플리케이션
&amp;rarr; Redis에 &quot;락 잡아줘&quot;, &quot;값 저장해줘&quot;, &quot;값 조회해줘&quot;라고 요청하는 쪽&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Spring Boot 안에서 Redis에 요청을 보내는 객체를&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Redis client&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;라고 부른다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RedissonClient&lt;span style=&quot;text-align: start;&quot;&gt;는 그중에서도 Redisson 라이브러리가 제공하는 클라이언트&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779883929674&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redissonClient.getLock(&quot;stock:decrease:123&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이건 Java 코드 입장에서는 메서드 호출이지만, 실제로는 RedissonClient가 Redis 서버와 통신해서 분산 락을 사용할 수 있게 해준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;비유&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779883647622&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Redis 서버 = 창고
Redisson = 창고를 쓰기 쉽게 해주는 Java 라이브러리
RedissonClient = 창고 출입증 + 창고 직원 호출 창구
RLock = 창고 안에 있는 특정 자물쇠&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;RedissonClient Bean 설정 클래스 추가&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;product/src/main/java/com/hubEleven/stock/infrastructure/configuration/RedissonConfig.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779853857381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RedissonConfig {

    @Bean(destroyMethod = &quot;shutdown&quot;)
    public RedissonClient redissonClient(
            @Value(&quot;${spring.data.redis.host:localhost}&quot;) String host,
            @Value(&quot;${spring.data.redis.port:6379}&quot;) int port) {

        Config config = new Config();
        config.useSingleServer()
                .setAddress(&quot;redis://&quot; + host + &quot;:&quot; + port);

        return Redisson.create(config);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;역할&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;yml 파일의 Redis host/port 설정을 읽어서&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779883824740&quot; class=&quot;vim&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;RedissonClient&lt;/span&gt;를 Spring Bean으로 등록&lt;/li&gt;
&lt;li&gt;이후 락 구현체에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;RedissonClient&lt;/span&gt;를 주입받을 수 있게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;무엇을 적용하는지&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring이 관리하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;RedissonClient&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Bean을 만든다.&lt;/li&gt;
&lt;li&gt;아까 설정한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;spring.data.redis.host&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;spring.data.redis.port&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 읽는다.&lt;/li&gt;
&lt;li&gt;Redis 주소를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;redis://host:port&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 Redisson에 넘긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 직접&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;로 만들지 않고,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;@Bean&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;으로 등록해서 주입받는 이유는?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;RedissonStockLockManager&lt;/span&gt;에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;RedissonClient&lt;/span&gt;를 주입받아야 한다.&lt;/li&gt;
&lt;li&gt;매번 락을 쓸 때마다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;Redisson.create(...)&lt;/span&gt;를 새로 만들면 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779886465872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;요청 100개
&amp;rarr; RedissonClient 100개 생성
&amp;rarr; Redis 연결도 100번 준비
&amp;rarr; 내부 리소스도 100번 준비
&amp;rarr; 끝나고 shutdown 안 하면 리소스 누수 가능&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;RedissonClient&lt;/span&gt;는 Redis 연결 리소스를 갖는 객체라 애플리케이션 전체에서 하나의 Bean으로 관리해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779886493369&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;애플리케이션 시작
&amp;rarr; RedissonClient 1개 생성
&amp;rarr; Spring이 Bean으로 보관
&amp;rarr; RedissonStockLockManager에 주입
&amp;rarr; 재고 감소 요청마다 같은 RedissonClient 재사용
&amp;rarr; 애플리케이션 종료 시 shutdown&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;destroyMethod = &quot;shutdown&quot;&lt;/span&gt;을 넣으면 애플리케이션 종료 시 Redisson 연결도 정상 종료된다.&lt;/li&gt;
&lt;li&gt;요약 : RedissonClient를 애플리케이션 전체에서 재사용하고, Spring이&amp;nbsp;생성/주입/종료까지&amp;nbsp;관리하게&amp;nbsp;하려고&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✅ Spring Bean&lt;br /&gt;Bean = Spring이 생성하고 관리하는 객체&lt;br /&gt;Spring Container = Bean들을 보관하고 필요한 곳에 주입해주는 공간&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;왜&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;decreaseStock()&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;안에서 매번 아래처럼 만들면 안 될까?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779854381303&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RedissonClient redissonClient = Redisson.create(config);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Redisson 공식 문서에서는 먼저&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Config&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 만들고,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Redisson.create(config)&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;인스턴스를 만든 뒤, 그 인스턴스에서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RLock&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RMap&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;같은 Redis 기반 객체를 가져오는 흐름을 보여준다. (&lt;a href=&quot;https://redisson.pro/docs/getting-started/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redisson 공식 문서&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;decreaseStock()&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;은 주문 요청마다 호출되고 그 안에서 매번&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Redisson.create(config)&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 하면 요청마다 Redis 클라이언트를 새로 만들게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이 경우의 문제점은 :&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Redis 연결 리소스를 매 요청마다 생성&lt;/p&gt;
&lt;pre id=&quot;code_1779886615728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Redis 서버 주소 설정
네트워크 연결 준비
connection pool 준비
명령을 보내고 응답을 받는 내부 처리 구조
timeout/retry 설정
내부 스레드 리소스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 요청마다 Redis 연결 관리자를 새로 만드는 비용이 든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redisson.create(config)는 &amp;ldquo;그냥 객체 하나 생성&amp;rdquo;이 아니라,&amp;nbsp;Redis를 사용할 준비 세트를 만드는 작업이다.&lt;/li&gt;
&lt;li&gt;그래서 요청마다 이걸 만들면 매번 준비 작업을 반복한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779887674775&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Redis 서버 주소 확인
Redis와 통신할 네트워크 연결 준비
명령을 보낼 내부 구조 준비
응답을 받을 내부 구조 준비
연결 풀 준비
타임아웃 설정 준비&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 동시 요청이 많을수록 불필요한 객체와 연결 리소스가 늘어난다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;각&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;RedissonClient&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;는 내부적으로 Redis와 통신할 준비를 한다. &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;그러면 메모리도 쓰고, 네트워크 연결도 쓰고, 내부 스레드 같은 자원도 쓸 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 매번&amp;nbsp;shutdown&amp;nbsp;해주지&amp;nbsp;않으면&amp;nbsp;연결/스레드&amp;nbsp;리소스가&amp;nbsp;남을&amp;nbsp;수&amp;nbsp;있음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RedissonClient 생성 과정을 비유해보자면, RedissonClient 생성 = 호텔 방 하나 빌림&lt;/li&gt;
&lt;li&gt;그러면 사용 끝난 뒤 반드시 체크아웃해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779888026640&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redissonClient.shutdown();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;shutdown 안 하면? 사용 안 하는 방이 계속 점유되고, 이 상태가 '리소스가 남는다'는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. decreaseStock()이&amp;nbsp;재고&amp;nbsp;감소뿐&amp;nbsp;아니라&amp;nbsp;Redis&amp;nbsp;클라이언트&amp;nbsp;생성&amp;nbsp;책임까지&amp;nbsp;갖게&amp;nbsp;됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 테스트에서&amp;nbsp;RedissonClient를&amp;nbsp;가짜&amp;nbsp;객체나&amp;nbsp;테스트용&amp;nbsp;락으로&amp;nbsp;바꾸기&amp;nbsp;어려워짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗️따라서 애플리케이션이 시작할 때, RedissonClient 객체 하나를 만들어 Spring Bean 으로 등록해두고 재사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779887037105&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RedissonClient redissonClient = Redisson.create(config);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RedissonClient 객체가 Spring Bean이 됨&lt;/li&gt;
&lt;li&gt;Spring Container가 이 객체를 보관함&lt;/li&gt;
&lt;li&gt;필요한&amp;nbsp;클래스에&amp;nbsp;같은&amp;nbsp;객체를&amp;nbsp;주입함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;출처 &lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Redisson Docs :&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://redisson.pro/docs/overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redisson.pro/docs/overview/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1779884133029&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Overview - Redisson Reference Guide&quot; data-og-description=&quot;Overview Redisson is the Java Client and Real-Time Data Platform for Valkey and Redis. Providing the most convenient and easiest way to work with Valkey or Redis. Redisson objects provide an abstraction layer between Valkey or Redis and your Java code, whi&quot; data-og-host=&quot;redisson.pro&quot; data-og-source-url=&quot;https://redisson.pro/docs/overview/&quot; data-og-url=&quot;https://redisson.pro/docs/overview/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://redisson.pro/docs/overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redisson.pro/docs/overview/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Overview - Redisson Reference Guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Overview Redisson is the Java Client and Real-Time Data Platform for Valkey and Redis. Providing the most convenient and easiest way to work with Valkey or Redis. Redisson objects provide an abstraction layer between Valkey or Redis and your Java code, whi&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redisson.pro&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Spring Docs : Bean Overview&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.enterprise.spring.io/spring-framework/reference/core/beans/definition.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.enterprise.spring.io/spring-framework/reference/core/beans/definition.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1779886945913&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Bean Overview :: Spring Framework&quot; data-og-description=&quot;Every bean has one or more identifiers. These identifiers must be unique within the container that hosts the bean. A bean usually has only one identifier. However, if it requires more than one, the extra ones can be considered aliases. In XML-based configu&quot; data-og-host=&quot;docs.enterprise.spring.io&quot; data-og-source-url=&quot;https://docs.enterprise.spring.io/spring-framework/reference/core/beans/definition.html&quot; data-og-url=&quot;https://docs.enterprise.spring.io/spring-framework/reference/core/beans/definition.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.enterprise.spring.io/spring-framework/reference/core/beans/definition.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.enterprise.spring.io/spring-framework/reference/core/beans/definition.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Bean Overview :: Spring Framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Every bean has one or more identifiers. These identifiers must be unique within the container that hosts the bean. A bean usually has only one identifier. However, if it requires more than one, the extra ones can be considered aliases. In XML-based configu&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.enterprise.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/555</guid>
      <comments>https://annovation.tistory.com/555#entry555comment</comments>
      <pubDate>Wed, 27 May 2026 22:00:31 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 동시성 문제 (Race Conditon) 해결하기</title>
      <link>https://annovation.tistory.com/554</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;동시성&amp;nbsp;문제&amp;nbsp;(Race&amp;nbsp;Conditon)&amp;nbsp;발생&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;StockCurrencyTest.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778158113123&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ActiveProfiles(&quot;test&quot;)
@SpringBootTest
public class StockCurrencyTest {

	@Autowired
	private StockServiceImpl stockServiceImpl;

	@Autowired
	private JpaStockRepository jpaStockRepository;

	@Autowired
	private JpaProductRepository jpaProductRepository;

	private Product product;

	@BeforeEach
	void setUp() {
		product = ProductFixture.createDefault();
		jpaProductRepository.saveAndFlush(product);

		Stock stock = StockFixture.createFromProductWithQuantity(product, 100);
		jpaStockRepository.saveAndFlush(stock);
	}

	@AfterEach
	void tearDown() {
		jpaStockRepository.deleteAll();
		jpaProductRepository.deleteAll();
	}

	@Test
	@DisplayName(&quot;재고 감소 - 100개 동시 요청 시 정확히 차감&quot;)
	void decreaseStock_when100ConcurrentRequests_thenSuccess() throws Exception {

		// given
		int threadCount = 100;

		ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

		CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		// when
		for (int i = 0; i &amp;lt; threadCount; i++) {
			executorService.submit(() -&amp;gt; {

				try {
					stockServiceImpl.decreaseStock(
						StockFixture.decreaseRequest(product, 1)
					);
				} finally {
					countDownLatch.countDown();
				}
			});
		}

		countDownLatch.await();

		// then
		Stock updatedStock = jpaStockRepository
			.findByProductId(product.getProductId())
			.orElseThrow();

		assertEquals(0, updatedStock.getQuantity());
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 생성 API에서 동시에 재고 차감 요청 발생&lt;/li&gt;
&lt;li&gt;같은 재고 값을 여러 트랜잭션이 읽으면서 Lost Update 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;원인 분석&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Stock.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778158195871&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void decreaseQuantity(int decreaseQuantity) {
    validateDecreaseQuantity(decreaseQuantity);

    if (this.quantity &amp;lt; decreaseQuantity) {
        throw new GlobalException(INSUFFICIENT_STOCK);
    }

    this.quantity -= decreaseQuantity;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회 후 수정 방식의 재고 차감 로직&lt;/li&gt;
&lt;li&gt;기본 트랜잭션 격리 수준에서는 같은 row를 순차적으로 갱신하지 않음&lt;/li&gt;
&lt;li&gt;서비스가 여러 인스턴스로 확장될 경우 JVM 내부 락은 무의미&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;해결하기&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Pessimistic Lock&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB row에 직접 쓰기 락을 걸어 동일 재고 차감 요청을 순차 처리&lt;/li&gt;
&lt;li&gt;DB 트랜잭션 안에서 정합성을 보장하므로 구현과 데이터 일관성 관리가 단순함&lt;/li&gt;
&lt;li&gt;단점은 동시 요청이 몰릴수록 락 대기 시간이 증가하고 DB 부하가 커질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Redisson Distributed Lock&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis에 productId 기준 락을 걸어 애플리케이션 진입 전부터 요청을 직렬화&lt;/li&gt;
&lt;li&gt;여러 Product 서비스 인스턴스가 떠 있어도 동일 Redis를 기준으로 락을 공유할 수 있음&lt;/li&gt;
&lt;li&gt;단점은 Redis 장애, 락 타임아웃, 락 해제 실패 같은 운영 고려사항이 추가됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) 블로그 : 티켓팅 서비스 동시성 제어) 낙관적 락의 한계와 Redis 분산 락으로의 전환&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@Jinpyo-An/티켓팅-서비스-동시성-제어-낙관적-락의-한계와-redis-분산-락으로의-전환-175093d17898&quot;&gt;https://medium.com/@Jinpyo-An/티켓팅-서비스-동시성-제어-낙관적-락의-한계와-redis-분산-락으로의-전환-175093d17898&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778157951937&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;티켓팅 서비스 동시성 제어: 낙관적 락의 한계와 Redis 분산 락으로의 전환&quot; data-og-description=&quot;들어가며: 10개의 좌석, 1,000명의 동시 접속&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@Jinpyo-An/티켓팅-서비스-동시성-제어-낙관적-락의-한계와-redis-분산-락으로의-전환-175093d17898&quot; data-og-url=&quot;https://medium.com/@Jinpyo-An/%ED%8B%B0%EC%BC%93%ED%8C%85-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EC%9D%98-%ED%95%9C%EA%B3%84%EC%99%80-redis-%EB%B6%84%EC%82%B0-%EB%9D%BD%EC%9C%BC%EB%A1%9C%EC%9D%98-%EC%A0%84%ED%99%98-175093d17898&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/8jPDj/dJMb9llbguz/KoaxAXpt7uZKpgJJta2fkk/img.png?width=1200&amp;amp;height=721&amp;amp;face=0_0_1200_721&quot;&gt;&lt;a href=&quot;https://medium.com/@Jinpyo-An/티켓팅-서비스-동시성-제어-낙관적-락의-한계와-redis-분산-락으로의-전환-175093d17898&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@Jinpyo-An/티켓팅-서비스-동시성-제어-낙관적-락의-한계와-redis-분산-락으로의-전환-175093d17898&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/8jPDj/dJMb9llbguz/KoaxAXpt7uZKpgJJta2fkk/img.png?width=1200&amp;amp;height=721&amp;amp;face=0_0_1200_721');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;티켓팅 서비스 동시성 제어: 낙관적 락의 한계와 Redis 분산 락으로의 전환&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며: 10개의 좌석, 1,000명의 동시 접속&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/554</guid>
      <comments>https://annovation.tistory.com/554#entry554comment</comments>
      <pubDate>Mon, 11 May 2026 23:20:47 +0900</pubDate>
    </item>
    <item>
      <title>Codex Chat 삭제하는 방법 (MacOS)</title>
      <link>https://annovation.tistory.com/553</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Codex 저장 구조&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Codex는 로컬에 세션 파일을 저장한다.&lt;/li&gt;
&lt;li&gt;해당 파일을 앱 UI 내에서 삭제하는 기능은 아직 제공되지 않고 있기 때문에 로컬에 저장된 파일 경로로 직접 삭제해야한다.&lt;/li&gt;
&lt;li&gt;macOS 기본 경로는 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778046047883&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;~/.codex/sessions
~/.codex/archived_sessions&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;해결 방법 : archived chat 직접 삭제하기&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Finder 로 삭제하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Codex 앱에서 삭제할 chat을 Archive Chat 으로 옮긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Finder 를 연다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. ⌘ + Shift + G를 누른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 아래 경로로 이동한다.&lt;/p&gt;
&lt;pre id=&quot;code_1778046361980&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;~/.codex/archived_sessions/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 삭제할 archived session 파일을 휴지통으로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Terminal 로 삭제하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Codex 앱에서 삭제할 chat을 Archive Chat 으로 옮긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. archived session 목록을 확인하려면&lt;/p&gt;
&lt;pre id=&quot;code_1778046517156&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd ~/.codex/archived_sessions
ls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 특정 파일만 삭제하려면&lt;/p&gt;
&lt;pre id=&quot;code_1778046532062&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rm 파일명&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 예시&lt;/p&gt;
&lt;pre id=&quot;code_1778046557628&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rm session-abc123.jsonl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 보관된 채팅을 전부 삭제하려면&lt;/p&gt;
&lt;pre id=&quot;code_1778046578478&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rm -rf ~/.codex/archived_sessions/*&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;주의할 점&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 채팅은 백업&lt;/li&gt;
&lt;li&gt;archived_sessions 전체 삭제 시 복구 어려움&lt;/li&gt;
&lt;li&gt;sessions 폴더와 archived_sessions 폴더를 혼동하지 않기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. OpenAI Developers&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.openai.com/codex/app/troubleshooting&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.openai.com/codex/app/troubleshooting&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778046116759&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Troubleshooting &amp;ndash; Codex app | OpenAI Developers&quot; data-og-description=&quot;FAQ and fixes for common Codex app issues&quot; data-og-host=&quot;developers.openai.com&quot; data-og-source-url=&quot;https://developers.openai.com/codex/app/troubleshooting&quot; data-og-url=&quot;https://developers.openai.com/codex/app/troubleshooting&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oWcX4/dJMb9lla4Lc/kQ0qFxzyujMJKX3TzAK0YK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/vngbo/dJMb9kmgD9S/lALVBTJCs2fcUUIruCSRXk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://developers.openai.com/codex/app/troubleshooting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.openai.com/codex/app/troubleshooting&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oWcX4/dJMb9lla4Lc/kQ0qFxzyujMJKX3TzAK0YK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/vngbo/dJMb9kmgD9S/lALVBTJCs2fcUUIruCSRXk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Troubleshooting &amp;ndash; Codex app | OpenAI Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;FAQ and fixes for common Codex app issues&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.openai.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Tips ✨</category>
      <category>Tips</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/553</guid>
      <comments>https://annovation.tistory.com/553#entry553comment</comments>
      <pubDate>Wed, 6 May 2026 19:50:33 +0900</pubDate>
    </item>
    <item>
      <title>[block-server] 휴지통 자동 삭제 기능 구현 (2)</title>
      <link>https://annovation.tistory.com/551</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Application&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;DemoApplication.java&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-ab779d4521dde4efe0f3ff659cccdca4ef8a83a094859cc2572c010cccf49446&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PR Link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1774707286295&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&quot; data-og-description=&quot;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-ab779d4521dde4efe0f3ff659cccdca4ef8a83a094859cc2572c010cccf49446&quot; data-og-url=&quot;https://github.com/jho951/Block-server/pull/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/9Ufai/dJMb9cBH0M6/LZ2TNVDVwZuozzeBESqMuK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/babytW/dJMb9gxljvj/Tn0mKF76XKAn1KARA71eS0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-ab779d4521dde4efe0f3ff659cccdca4ef8a83a094859cc2572c010cccf49446&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-ab779d4521dde4efe0f3ff659cccdca4ef8a83a094859cc2572c010cccf49446&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/9Ufai/dJMb9cBH0M6/LZ2TNVDVwZuozzeBESqMuK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/babytW/dJMb9gxljvj/Tn0mKF76XKAn1KARA71eS0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Code&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774707307988&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.documents;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;코드 해석&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;@EnableScheduling
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Spring에서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;@Scheduled&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;가 붙은 메서드를 찾아서 주기적으로 실행하도록 켜는 설정&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Spring이 애플리케이션 시작 시 스케줄링 기능을 활성화하고, 예를 들어&amp;nbsp;아래 나오는&amp;nbsp;&lt;/span&gt;DocumentTrashPurgeScheduler.java&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;같은 Spring Bean 안의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;@Scheduled&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;메서드를 등록해서 실행한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Spring Javadoc : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/5.3.37/javadoc-api/org/springframework/scheduling/annotation/EnableScheduling.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@EnableScheduling&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Schedular&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;DocumentTrashPurgeScheduler.java&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-1b3111c7b77047d90f77b1e1d160394efbd8ac41134fdd3b8f59d1d3ceda842f&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PR Link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1774707431279&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&quot; data-og-description=&quot;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-1b3111c7b77047d90f77b1e1d160394efbd8ac41134fdd3b8f59d1d3ceda842f&quot; data-og-url=&quot;https://github.com/jho951/Block-server/pull/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1nHx9/dJMb9bv2czW/zNgRGAP6eawIeW66F283Dk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/c2TCPZ/dJMb9frFl6B/dOyygU7bArpsTPIRSj5Bb0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-1b3111c7b77047d90f77b1e1d160394efbd8ac41134fdd3b8f59d1d3ceda842f&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-1b3111c7b77047d90f77b1e1d160394efbd8ac41134fdd3b8f59d1d3ceda842f&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1nHx9/dJMb9bv2czW/zNgRGAP6eawIeW66F283Dk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/c2TCPZ/dJMb9frFl6B/dOyygU7bArpsTPIRSj5Bb0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Code&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774707443103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.documents;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.documents.service.DocumentService;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class DocumentTrashPurgeScheduler {

	private final DocumentService documentService;

	@Scheduled(fixedDelay = 60000)
	public void purgeExpiredTrash() {
		documentService.purgeExpiredTrash();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;코드 해석&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이 클래스는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;휴지통에 들어간 문서 중 만료된 문서를 주기적으로 정리하는 스케줄러 역할을 한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Spring의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;@Scheduled&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 이용해서 일정 주기마다 휴지통 만료 문서를 자동 삭제하도록 만든 배치성 컴포넌트이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;실행될 때 직접 삭제 로직을 구현하지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;documentService.purgeExpiredTrash()&lt;/span&gt;를 호출함&lt;/li&gt;
&lt;li&gt;즉, 스케줄링 책임과 비즈니스 삭제 책임을 분리한 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;코드 설명&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;@Component : &lt;/span&gt;Spring이 관리하는 Bean&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588);&quot;&gt;@Scheduled(fixedDelay = 60000)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이전 실행이 끝난 뒤 60초 후 다시 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;클래스를 따로 둔 이유&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄링은 실행 트리거 역할, 실제 정책 판단과 DB 삭제는 서비스 계층 역할로 구분하여 구현&lt;/li&gt;
&lt;li&gt;이렇게 분리하면 테스트가 쉬워진다. 즉, 서비스는 스케줄러 없이도 단위 테스트가 가능해진다.&lt;/li&gt;
&lt;li&gt;나중에 수동 실행(휴지통 비즈니스 로직 직접 호출), 관리자 API, 다른 배치 시스템으로 바꿔도 서비스 재사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588); color: #333333; text-align: start;&quot;&gt;fixedDelay&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ㅇ&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588); color: #ffffff; text-align: start;&quot;&gt;documentService.purgeExpiredTrash()&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.0470588); color: #ffffff; text-align: start;&quot;&gt;documentService.purgeExpiredTrash()&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Service&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;DocumentServiceImpl.java&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-47569d612045c6da04f60bf5ce8b1e8171cb00be301560c2a1fb6ba5c71cf79b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PR Link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1774713250720&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&quot; data-og-description=&quot;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-47569d612045c6da04f60bf5ce8b1e8171cb00be301560c2a1fb6ba5c71cf79b&quot; data-og-url=&quot;https://github.com/jho951/Block-server/pull/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dI7too/dJMb9cBH0Z3/7feKkiyQIl7kS0VOKCJnH1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dpQST8/dJMb9iaQ2z6/OkImLLQU0lvxRlzXRhPJ21/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-47569d612045c6da04f60bf5ce8b1e8171cb00be301560c2a1fb6ba5c71cf79b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-47569d612045c6da04f60bf5ce8b1e8171cb00be301560c2a1fb6ba5c71cf79b&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dI7too/dJMb9cBH0Z3/7feKkiyQIl7kS0VOKCJnH1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dpQST8/dJMb9iaQ2z6/OkImLLQU0lvxRlzXRhPJ21/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Code&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774713295023&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
@Transactional
public void purgeExpiredTrash() {
    LocalDateTime expiredAt = LocalDateTime.now().minusMinutes(TRASH_RETENTION_MINUTES);
    List&amp;lt;Document&amp;gt; expiredTrashRoots = documentRepository.findExpiredTrashRoots(expiredAt);

    for (Document expiredTrashRoot : expiredTrashRoots) {
        documentRepository.delete(expiredTrashRoot);
    }
}

...

private void validateTrashRestoreAvailable(Document document) {
    LocalDateTime restoreDeadline = document.getDeletedAt().plusMinutes(TRASH_RESTORE_AVAILABLE_MINUTES);
    LocalDateTime restoreDeadline = document.getDeletedAt().plusMinutes(TRASH_RETENTION_MINUTES);
    if (!LocalDateTime.now().isBefore(restoreDeadline)) {
        throw new BusinessException(BusinessErrorCode.DOCUMENT_NOT_FOUND);
    }
	}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Repository&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;DocumentRepository.java&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-a9f5f265dada3a449538247148f01ed946943c615ece07925b1c0977ce289881&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PR Link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1774713554220&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&quot; data-og-description=&quot;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-a9f5f265dada3a449538247148f01ed946943c615ece07925b1c0977ce289881&quot; data-og-url=&quot;https://github.com/jho951/Block-server/pull/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/XVl0b/dJMb9g5a3Xn/pgKqaam7op2XM9FyiE41K1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/G6h1S/dJMb9b3RXJI/a41AVl9wgGB5jjpkfz2eJK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-a9f5f265dada3a449538247148f01ed946943c615ece07925b1c0977ce289881&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jho951/Block-server/pull/43/changes/b1fd9c650c3bf41f07a11d4d84b56c5edb2b6101#diff-a9f5f265dada3a449538247148f01ed946943c615ece07925b1c0977ce289881&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/XVl0b/dJMb9g5a3Xn/pgKqaam7op2XM9FyiE41K1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/G6h1S/dJMb9b3RXJI/a41AVl9wgGB5jjpkfz2eJK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Code&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774713563576&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;&quot;&quot;
    select d
    from Document d
    left join d.parent p
    where d.deletedAt is not null
      and d.deletedAt &amp;lt;= :expiredAt
      and (
        p is null
        or p.deletedAt is null
      )
    order by
      d.deletedAt asc,
      d.createdAt asc,
      d.id asc
    &quot;&quot;&quot;)
List&amp;lt;Document&amp;gt; findExpiredTrashRoots(@Param(&quot;expiredAt&quot;) LocalDateTime expiredAt);&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Github repo&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jho951/Block-server/pull/43&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774707173958&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&quot; data-og-description=&quot;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jho951/Block-server/pull/43&quot; data-og-url=&quot;https://github.com/jho951/Block-server/pull/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xo47Z/dJMb82MCZTm/pRzs4137PkT81Bnl0uxYbK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bxPGsL/dJMb83SiJiR/3MkC8cIUQW1h5ZKwZHgy41/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server/pull/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jho951/Block-server/pull/43&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xo47Z/dJMb82MCZTm/pRzs4137PkT81Bnl0uxYbK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bxPGsL/dJMb83SiJiR/3MkC8cIUQW1h5ZKwZHgy41/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;feat: 문서 휴지통 자동 삭제 기능 추가 by hellonaeunkim &amp;middot; Pull Request #43 &amp;middot; jho951/Block-server&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Part (해당되는 것만 체크) BE FE Infra Docs Test #️⃣ 연관된 이슈 closes #41   작업 내용 1. 주요 변경 사항 요약 기존 문서 삭제 API를 soft delete에서 hard delete로 변경 문서를 휴지통으로 보내는 별&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/block-server</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/551</guid>
      <comments>https://annovation.tistory.com/551#entry551comment</comments>
      <pubDate>Sat, 28 Mar 2026 23:30:51 +0900</pubDate>
    </item>
    <item>
      <title>[block-server] 휴지통 자동 삭제 기능 구현 (1) : Spring Scheduling vs Spring Batch</title>
      <link>https://annovation.tistory.com/547</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Spring Scheduling&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;소제목&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Spring Batch&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;소제목&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;두꺼운 밑줄 스타일&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Scheduler (@Scheduled)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Spring Batch&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;목적&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;작업 실행 시점 관리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;대용량 데이터 처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&amp;ldquo;언제 실행할까&amp;rdquo;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&amp;ldquo;어떻게 처리할까&amp;rdquo;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;데이터 처리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;단순 로직&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;대량 데이터 처리 최적화&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;구조&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;메서드 단위 실행&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Job &amp;rarr; Step 구조&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;트랜잭션&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;직접 처리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;자동 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;실패 처리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;없음 (직접 구현)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;재시작, retry 지원&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;사용 난이도&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;쉬움&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;비교적 복잡&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Spring Framework Docs : Spring Scheduling&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/integration/scheduling.html?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774331611731&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Task Execution and Scheduling :: Spring Framework&quot; data-og-description=&quot;All Spring cron expressions have to conform to the same format, whether you are using them in @Scheduled annotations, task:scheduled-tasks elements, or someplace else. A well-formed cron expression, such as * * * * * *, consists of six space-separated time&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html?utm_source=chatgpt.com&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html?utm_source=chatgpt.com&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Task Execution and Scheduling :: Spring Framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;All Spring cron expressions have to conform to the same format, whether you are using them in @Scheduled annotations, task:scheduled-tasks elements, or someplace else. A well-formed cron expression, such as * * * * * *, consists of six space-separated time&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/block-server</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/547</guid>
      <comments>https://annovation.tistory.com/547#entry547comment</comments>
      <pubDate>Mon, 23 Mar 2026 23:41:56 +0900</pubDate>
    </item>
    <item>
      <title>[block-server] 프로젝트 구조</title>
      <link>https://annovation.tistory.com/545</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Structure&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Notion 스타일의 문서 계층과 블록 트리를 다루는 백엔드 서비스이며, Spring Boot 기반 Gradle 멀티모듈 구조로 책임을 분리한 것이 핵심!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774107231400&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;
  Block-server
├─&amp;nbsp;AGENTS.md
├─&amp;nbsp;README.md
├─&amp;nbsp;docker
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Dockerfile
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;docker-compose.yml
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;docker.sh
├─&amp;nbsp;docs
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;REQUIREMENTS.md
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;decisions
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;000-adr-template.md
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;README.md
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;discussions
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;000-strategy-review-template.md
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;README.md
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;roadmap
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;README.md
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;v2
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;blocks
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;block-delete.md
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;block-restore.md
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;runbook
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;DEBUG.md
├─&amp;nbsp;documents-api
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;build.gradle
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;src
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;main
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;com
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;documents
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;api
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;block
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockApiMapper.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockController.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;dto
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockResponse.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;CreateBlockRequest.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;UpdateBlockRequest.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;support
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;BlockJsonCodec.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;validation
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockContentValidator.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;ValidBlockContent.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;code
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;ErrorCode.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;SuccessCode.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;document
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;DocumentApiMapper.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;DocumentController.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;dto
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;CreateDocumentRequest.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;DocumentResponse.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;MoveDocumentRequest.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;UpdateDocumentRequest.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;support
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;DocumentJsonCodec.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;validation
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;DocumentMetaValidator.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;ValidDocumentMeta.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;dto
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BaseResponse.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;GlobalResponse.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;exception
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;GlobalException.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;GlobalExceptionHandler.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;workspace
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;WorkspaceController.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;dto
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;CreateWorkspaceRequest.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;WorkspaceResponse.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;common
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;SwaggerConfig.java
├─&amp;nbsp;documents-boot
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;build.gradle
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;src
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;main
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;com
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;documents
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;DemoApplication.java
├─&amp;nbsp;documents-core
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;build.gradle
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;src
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;main
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;com
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;documents
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;common
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;BaseEntity.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;domain
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Block.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockType.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Document.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;Workspace.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;exception
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BusinessErrorCode.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;BusinessException.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;service
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockService.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;DocumentService.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;WorkspaceService.java
├─&amp;nbsp;documents-infrastructur
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;src
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;main
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;com
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;documents
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;repository
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockRepository.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;DocumentRepository.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;WorkspaceRepository.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;service
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;BlockServiceImpl.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;DocumentServiceImpl.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;WorkspaceServiceImpl.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;support
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;OrderedSortKeyGenerator.java
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;TextNormalizer.java
└─&amp;nbsp;prompts
&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;0-initial-setup.md
&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;README.md
&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;explainer
&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;plan&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Explains&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;documents-boot&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;애플리케이션 실행 진입점, 프로필/환경설정, 패키징 담당&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;`DemoApplication.java`가&amp;nbsp;위치하며,&amp;nbsp;실제&amp;nbsp;서버&amp;nbsp;실행과&amp;nbsp;설정&amp;nbsp;로딩을&amp;nbsp;담당한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;즉,&amp;nbsp;다른&amp;nbsp;모듈이&amp;nbsp;기능을&amp;nbsp;제공하는&amp;nbsp;라이브러리라면&amp;nbsp;`documents-boot`는&amp;nbsp;그것들을&amp;nbsp;조립해서&amp;nbsp;실행&amp;nbsp;가능한&amp;nbsp;애플리케이션으로&amp;nbsp;만드는&amp;nbsp;역할을&amp;nbsp;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;documents-api&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;외부&amp;nbsp;요청을&amp;nbsp;직접&amp;nbsp;받는&amp;nbsp;계층이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이 모듈에는 다음과 같은 요소가 들어 있다.&lt;br /&gt;-&amp;nbsp;`Controller`:&amp;nbsp;문서,&amp;nbsp;블록,&amp;nbsp;워크스페이스&amp;nbsp;API&amp;nbsp;엔드포인트&lt;br /&gt;-&amp;nbsp;`dto`:&amp;nbsp;요청/응답&amp;nbsp;객체&lt;br /&gt;-&amp;nbsp;`validation`:&amp;nbsp;요청값&amp;nbsp;검증&amp;nbsp;로직&lt;br /&gt;-&amp;nbsp;`support`:&amp;nbsp;JSON&amp;nbsp;변환&amp;nbsp;보조&amp;nbsp;로직&lt;br /&gt;-&amp;nbsp;`code`,&amp;nbsp;`exception`:&amp;nbsp;공통&amp;nbsp;응답&amp;nbsp;코드와&amp;nbsp;예외&amp;nbsp;처리&lt;br /&gt;-&amp;nbsp;`SwaggerConfig`:&amp;nbsp;API&amp;nbsp;문서화&amp;nbsp;설정&lt;/li&gt;
&lt;li&gt;즉,&amp;nbsp;HTTP&amp;nbsp;요청을&amp;nbsp;받아&amp;nbsp;검증하고,&amp;nbsp;도메인&amp;nbsp;계층에&amp;nbsp;작업을&amp;nbsp;위임한&amp;nbsp;뒤,&amp;nbsp;다시&amp;nbsp;응답&amp;nbsp;형식으로&amp;nbsp;변환하는&amp;nbsp;역할을&amp;nbsp;맡는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;documents-core&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;text-align: start;&quot;&gt;서비스의&amp;nbsp;중심&amp;nbsp;규칙을&amp;nbsp;담는&amp;nbsp;계층이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;여기에는 다음이 위치한다.&lt;br /&gt;-&amp;nbsp;`domain`:&amp;nbsp;`Document`,&amp;nbsp;`Block`,&amp;nbsp;`Workspace`&amp;nbsp;같은&amp;nbsp;핵심&amp;nbsp;엔티티&lt;br /&gt;-&amp;nbsp;`service`:&amp;nbsp;도메인&amp;nbsp;서비스&amp;nbsp;인터페이스&lt;br /&gt;-&amp;nbsp;`exception`:&amp;nbsp;비즈니스&amp;nbsp;예외와&amp;nbsp;에러&amp;nbsp;코드&lt;br /&gt;-&amp;nbsp;`common`:&amp;nbsp;공통&amp;nbsp;엔티티&amp;nbsp;기반&amp;nbsp;클래스&lt;/li&gt;
&lt;li&gt;중요한 점은, 이 계층은 &quot;무엇을 해야 하는가&quot;를 정의한다는 것이다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;구체적으로&amp;nbsp;어떻게&amp;nbsp;저장하는지는&amp;nbsp;여기서&amp;nbsp;다루지&amp;nbsp;않고,&amp;nbsp;서비스&amp;nbsp;계약과&amp;nbsp;도메인&amp;nbsp;모델&amp;nbsp;중심으로&amp;nbsp;구조를&amp;nbsp;잡고&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;documents-infrastructure&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제&amp;nbsp;구현을&amp;nbsp;담당한다.&lt;/li&gt;
&lt;li&gt;이 모듈에는 다음이 있다.&lt;br /&gt;-&amp;nbsp;`repository`:&amp;nbsp;JPA&amp;nbsp;Repository&lt;br /&gt;-&amp;nbsp;`service`:&amp;nbsp;`documents-core`의&amp;nbsp;서비스&amp;nbsp;인터페이스&amp;nbsp;구현체&lt;br /&gt;- `support`: 정렬 키 생성기, 문자열 정규화 같은 보조 유틸리티&lt;/li&gt;
&lt;li&gt;예를 들어 문서 이동, 블록 생성, soft delete 같은 실제 비즈니스 처리는 이 계층의 `ServiceImpl`에서 수행된다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;즉,&amp;nbsp;`core`가&amp;nbsp;계약이라면&amp;nbsp;`infrastructure`는&amp;nbsp;그&amp;nbsp;계약의&amp;nbsp;실행체다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;문서화와&amp;nbsp;운영&amp;nbsp;보조&amp;nbsp;구조&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;루트에는 코드 외에도 협업과 기록을 위한 디렉터리가 따로 존재한다.&lt;br /&gt;-&amp;nbsp;`docs/REQUIREMENTS.md`:&amp;nbsp;현재&amp;nbsp;유효한&amp;nbsp;요구사항&amp;nbsp;기준서&lt;br /&gt;-&amp;nbsp;`docs/decisions`:&amp;nbsp;채택된&amp;nbsp;기술&amp;nbsp;결정(ADR)&lt;br /&gt;-&amp;nbsp;`docs/discussions`:&amp;nbsp;전략&amp;nbsp;검토,&amp;nbsp;비교,&amp;nbsp;회의&amp;nbsp;메모&lt;br /&gt;-&amp;nbsp;`docs/roadmap`:&amp;nbsp;후속&amp;nbsp;버전&amp;nbsp;확장&amp;nbsp;계획&lt;br /&gt;-&amp;nbsp;`docs/runbook`:&amp;nbsp;재현&amp;nbsp;가능한&amp;nbsp;디버깅&amp;nbsp;절차&lt;br /&gt;-&amp;nbsp;`prompts`:&amp;nbsp;AI&amp;nbsp;작업&amp;nbsp;로그와&amp;nbsp;설명&amp;nbsp;문서&lt;/li&gt;
&lt;li&gt;이 구조는 단순히 코드를 작성하는 데서 끝나지 않고,&amp;nbsp;&amp;nbsp;&quot;왜&amp;nbsp;이렇게&amp;nbsp;만들었는지&quot;,&amp;nbsp;&quot;무엇을&amp;nbsp;결정했는지&quot;,&amp;nbsp;&quot;다음엔&amp;nbsp;무엇을&amp;nbsp;할지&quot;까지&amp;nbsp;함께&amp;nbsp;관리하려는&amp;nbsp;의도가&amp;nbsp;반영된&amp;nbsp;설계다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;실행&amp;nbsp;환경&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;루트의 `docker/` 디렉터리는 로컬 실행 환경을 담당한다.&lt;br /&gt;-&amp;nbsp;`Dockerfile`&lt;br /&gt;-&amp;nbsp;`docker-compose.yml`&lt;br /&gt;-&amp;nbsp;`docker.sh`&lt;/li&gt;
&lt;li&gt;즉, 애플리케이션과 DB를 포함한 실행 환경을 빠르게 재현할 수 있도록 준비된 구조다.&lt;/li&gt;
&lt;li&gt;`docker-compose.yml`을 활용해 애플리케이션과 MySQL 같은 실행 의존성을 함께 올릴 수 있도록 구성해, 로컬 개발 환경을 빠르게 재현할 수 있게 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Github Repo&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jho951/Block-server&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774106475532&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jho951/Block-server: server drawer&quot; data-og-description=&quot;server drawer. Contribute to jho951/Block-server development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jho951/Block-server&quot; data-og-url=&quot;https://github.com/jho951/Block-server&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4BYEK/dJMb87NVGTK/nPDxQRxCi35CC1t6OnfMeK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/sRGaH/dJMb89ycQMF/ll4dlIA0jmdmi5WBmpxW11/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jho951/Block-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jho951/Block-server&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4BYEK/dJMb87NVGTK/nPDxQRxCi35CC1t6OnfMeK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/sRGaH/dJMb89ycQMF/ll4dlIA0jmdmi5WBmpxW11/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jho951/Block-server: server drawer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;server drawer. Contribute to jho951/Block-server development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/block-server</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/545</guid>
      <comments>https://annovation.tistory.com/545#entry545comment</comments>
      <pubDate>Fri, 20 Mar 2026 23:49:54 +0900</pubDate>
    </item>
    <item>
      <title>[바이브 코딩] MacOS Codex CLI 설치</title>
      <link>https://annovation.tistory.com/542</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;1. Node.js 설치 확인&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Node.js 설치 확인&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773675531898&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;node -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;zsh: command not found: node 처럼 아무것도 안나온다면, brew 로 설치&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773675577196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install node&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;2. Codex CLI 설치&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Codex CLI 설치&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773675717966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -g @openai/codex&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;node 버전이 오래된 경우 Error 가 뜨기도 한다.&lt;/li&gt;
&lt;li&gt;ex. node -v 명령어를 통해 확인한 버전이 v20.11.0 였는데 오류가 나서 brew upgrade node 후 다시 진행했더니 오류가 없었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;설치 확인&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773675760851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;codex --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-03-17 at 12.43.23 AM.png&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8khtq/dJMcajaoi3w/yVkPdvpHtjQE5m8nkhocak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8khtq/dJMcajaoi3w/yVkPdvpHtjQE5m8nkhocak/img.png&quot; data-alt=&quot;설치된 codex cli 버전 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8khtq/dJMcajaoi3w/yVkPdvpHtjQE5m8nkhocak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8khtq%2FdJMcajaoi3w%2FyVkPdvpHtjQE5m8nkhocak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;83&quot; data-filename=&quot;Screenshot 2026-03-17 at 12.43.23 AM.png&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설치된 codex cli 버전 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;계정 연동&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773675960775&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;터미널에 codex 입력 후 아래와 같이 계정을 연동할 수 있는 옵션이 뜬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-03-17 at 12.46.58 AM.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lc1vb/dJMcacJaoLi/2q7e3xQz5NrkrYTKZR8cm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lc1vb/dJMcacJaoLi/2q7e3xQz5NrkrYTKZR8cm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lc1vb/dJMcacJaoLi/2q7e3xQz5NrkrYTKZR8cm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flc1vb%2FdJMcacJaoLi%2F2q7e3xQz5NrkrYTKZR8cm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2100&quot; height=&quot;314&quot; data-filename=&quot;Screenshot 2026-03-17 at 12.46.58 AM.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mac 의 경우 Codex 앱에 로그인이 되어있으면 추가로 웹페이지로 리다이렉트해서 로그인하는 과정 없이 바로 연동이 되는 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-03-17 at 12.49.52 AM.png&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pKZqe/dJMcach494A/AtkmKGgxfWtp7FcPtml4m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pKZqe/dJMcach494A/AtkmKGgxfWtp7FcPtml4m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pKZqe/dJMcach494A/AtkmKGgxfWtp7FcPtml4m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpKZqe%2FdJMcach494A%2FAtkmKGgxfWtp7FcPtml4m0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2196&quot; height=&quot;582&quot; data-filename=&quot;Screenshot 2026-03-17 at 12.49.52 AM.png&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;CLI 명령어&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Codex 실행&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773676351698&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 바로 프롬프트 명령 가능&lt;/p&gt;
&lt;pre id=&quot;code_1773676509038&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;codex &quot;Explain this project&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Codex 종료&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773676574314&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/quit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;모델 변경&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773676402848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/model&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;빠른 응답, 가벼운 작업&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773676477233&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/fast&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773676883223&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/fast
Explain this file&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;깊은 분석, 복잡한 작업&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773676865643&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/slow&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773676904075&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/slow
Find architecture problems in this repo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;현재 변경사항 리뷰&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773676608564&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/review --uncommitted&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;터미널 명령 직접 실행&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773676652996&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;!ls
!pwd
!git status&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Codex 안에서 앞에 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 붙이면 &lt;/span&gt;&lt;b&gt;실제 shell command 실행&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 블로그 : 맥북에 Codex CLI 설치하기 (feat. Ghat GPT CLI 설치 방법)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ratatou2.tistory.com/333&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ratatou2.tistory.com/333&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773676219804&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;맥북에 Codex CLI 설치하기 (feat. Ghat GPT CLI 설치 방법)&quot; data-og-description=&quot;Ghat GPT 유료를 쓰면 Codex가 무료..?난 몰랐다 GPT 유료버전을 쓰면 Codex를 준다는 것을 ㅠ지금이라도 알았으니까 얼른 써야지 뭐...Codex는 Cluade CLI처럼 터미널 창에서 쓸 수 있는 AI 코딩툴? 같은 것&quot; data-og-host=&quot;ratatou2.tistory.com&quot; data-og-source-url=&quot;https://ratatou2.tistory.com/333&quot; data-og-url=&quot;https://ratatou2.tistory.com/333&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3a7mU/dJMb8952pdH/NKvEuGY7gFOx01BhK6f7EK/img.png?width=800&amp;amp;height=774&amp;amp;face=0_0_800_774,https://scrap.kakaocdn.net/dn/bxyUCt/dJMb88F3HiG/PQlhpL3nYL1nt7U2331KG1/img.png?width=800&amp;amp;height=774&amp;amp;face=0_0_800_774,https://scrap.kakaocdn.net/dn/dffO6p/dJMb84p7vxb/8WJKhcK4W1VB0foEa8MyW0/img.png?width=1032&amp;amp;height=1024&amp;amp;face=0_0_1032_1024&quot;&gt;&lt;a href=&quot;https://ratatou2.tistory.com/333&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ratatou2.tistory.com/333&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3a7mU/dJMb8952pdH/NKvEuGY7gFOx01BhK6f7EK/img.png?width=800&amp;amp;height=774&amp;amp;face=0_0_800_774,https://scrap.kakaocdn.net/dn/bxyUCt/dJMb88F3HiG/PQlhpL3nYL1nt7U2331KG1/img.png?width=800&amp;amp;height=774&amp;amp;face=0_0_800_774,https://scrap.kakaocdn.net/dn/dffO6p/dJMb84p7vxb/8WJKhcK4W1VB0foEa8MyW0/img.png?width=1032&amp;amp;height=1024&amp;amp;face=0_0_1032_1024');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;맥북에 Codex CLI 설치하기 (feat. Ghat GPT CLI 설치 방법)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ghat GPT 유료를 쓰면 Codex가 무료..?난 몰랐다 GPT 유료버전을 쓰면 Codex를 준다는 것을 ㅠ지금이라도 알았으니까 얼른 써야지 뭐...Codex는 Cluade CLI처럼 터미널 창에서 쓸 수 있는 AI 코딩툴? 같은 것&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ratatou2.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/block-server</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/542</guid>
      <comments>https://annovation.tistory.com/542#entry542comment</comments>
      <pubDate>Tue, 17 Mar 2026 22:50:39 +0900</pubDate>
    </item>
    <item>
      <title>[바이브 코딩] AGENTS.md 로 코딩 에이전트 활용하기</title>
      <link>https://annovation.tistory.com/541</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;바이브 코딩&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;AGENTS.md&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAI Codex 팀에서 처음 발표한 AI 에이전트 용 표준 규칙 파일&lt;/li&gt;
&lt;li&gt;프로젝트에서 AI 에이전트에게 필요한 맥락과 지침을 주입시켜주는 간단한 마크다운 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;AI 에이전트가 뭐지? (&lt;a href=&quot;https://openai.com/ko-KR/index/introducing-codex/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 자료&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순히 답하는 AI가 아니라 실제 작업을 스스로 수행하고 끝까지 완료하는 실행형 AI 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;효율적인 AGENTS.md 구조&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;하나의 AGENTS.md는 500줄 이내로 유지&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM 모델의 특성상 컨텍스트 윈도우 사이즈가 한정되어 있기 때문에 에이전트 규칙이 차지하는 정의 때문에 컨텍스트 낭비가 생길 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❓컨텍스트&lt;br /&gt;LLM이 쓸 수 있는 제한된 공간&lt;br /&gt;&lt;br /&gt;❓LLM&lt;br /&gt;자연어를 토큰과 숫자(벡터)로 변환한 뒤, 이전 문맥을 기반으로 다음에 올 &amp;lsquo;단어(토큰)&amp;rsquo;의 확률을 계산해 생성하는 모델&lt;br /&gt;ex. 나는 밥을 먹었다. (자연어) -&amp;gt; 나는 / 밥을 / 먹었다. (토큰) -&amp;gt; 숫자 (벡터)로 변환 -&amp;gt; 나는 밥을 ___. 에서 먹었다를 예측&lt;br /&gt;&lt;br /&gt;❓컨텍스트 원도우 사이즈&lt;br /&gt;LLM이 한 번에 읽고 기억할 수 있는 텍스트 용량의 한계&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;큰 프로젝트에서는 여러 개의 조합 가능한 작은 규칙으로 나누어 정의&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 중첩된 파일로 구조적으로 규칙 파일 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-04-02 at 2.22.19 PM.png&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Itg8o/dJMcacCCzaw/e1wNXSZQkRG7R69bkS1gyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Itg8o/dJMcacCCzaw/e1wNXSZQkRG7R69bkS1gyK/img.png&quot; data-alt=&quot;출처 : https://www.youtube.com/watch?v=LHx_EFoLonQ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Itg8o/dJMcacCCzaw/e1wNXSZQkRG7R69bkS1gyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FItg8o%2FdJMcacCCzaw%2Fe1wNXSZQkRG7R69bkS1gyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1690&quot; height=&quot;796&quot; data-filename=&quot;Screenshot 2026-04-02 at 2.22.19 PM.png&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://www.youtube.com/watch?v=LHx_EFoLonQ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;AGENTS.md 파일도 소스코드와 함께 버전관리 하기&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드가 변경되었을 때, 관련된 AGENTS.md 파일도 함께 수정하여 버전 관리를 코드와 같이 해주는 것을 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❓버전 관리&lt;br /&gt;파일의 변화 과정을 시간순으로 저장하고 관리하는 것&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 에이전트가 실제로 코드를 생성하고 작업할 때 AGENTS.md 파일이 큰 영향을 미치기 때문에 코드 리뷰에서 코드만 리뷰하는 것이 아닌, AGENTS.md 파일도 함께 리뷰 하는 것을 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;신규 팀원 온보딩에 AGENTS.md 활용하기&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AGENTS.md 파일에는 아래와 같은 여러 가지 정보가 포함되어 있기 때문에 프로젝트의 코드베이스를 굉장히 빠르게 파악하고 작업을 시작할 수 있는 역할을 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-04-03 at 9.45.46 PM.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKQqlX/dJMcagLOFDd/Lv4bqMa4KIkpcbjPr9rdB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKQqlX/dJMcagLOFDd/Lv4bqMa4KIkpcbjPr9rdB1/img.png&quot; data-alt=&quot;출처 : https://www.youtube.com/watch?v=LHx_EFoLonQ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKQqlX/dJMcagLOFDd/Lv4bqMa4KIkpcbjPr9rdB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKQqlX%2FdJMcagLOFDd%2FLv4bqMa4KIkpcbjPr9rdB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;481&quot; height=&quot;169&quot; data-filename=&quot;Screenshot 2026-04-03 at 9.45.46 PM.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://www.youtube.com/watch?v=LHx_EFoLonQ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;한 번 작성으로 끝나는 것이 아니라 계속해서 반복적으로 개선하기&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 구조 변경, 새로운 기술 스택 추가, 정해진 규칙 준수 여부를 지속적으로 모니터링하고 업데이트 하는 것이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;읽어 보기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 컬리 기술 블로그 : &lt;a href=&quot;https://helloworld.kurly.com/blog/vibe-coding-with-claude-code/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://helloworld.kurly.com/blog/vibe-coding-with-claude-code/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773643591277&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Claude Code를 활용한 예측 가능한 바이브 코딩 전략&quot; data-og-description=&quot;에이전트 주도 개발에서 발생하는 문제를 해결하고, 컨텍스트를 정밀하게 관리하는 방법&quot; data-og-host=&quot;helloworld.kurly.com&quot; data-og-source-url=&quot;https://helloworld.kurly.com/blog/vibe-coding-with-claude-code/&quot; data-og-url=&quot;http://thefarmersfront.github.io/blog/vibe-coding-with-claude-code/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b71HqR/dJMb8U8Sm9r/LPFT4eXvkEbrL8Dx274VN1/img.png?width=1452&amp;amp;height=1452&amp;amp;face=0_0_1452_1452&quot;&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/vibe-coding-with-claude-code/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://helloworld.kurly.com/blog/vibe-coding-with-claude-code/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b71HqR/dJMb8U8Sm9r/LPFT4eXvkEbrL8Dx274VN1/img.png?width=1452&amp;amp;height=1452&amp;amp;face=0_0_1452_1452');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Claude Code를 활용한 예측 가능한 바이브 코딩 전략&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;에이전트 주도 개발에서 발생하는 문제를 해결하고, 컨텍스트를 정밀하게 관리하는 방법&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;helloworld.kurly.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 유튜브 - 개발동생 : &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;코딩 에이전트 성능을 20배 올리는 가장 쉬운 방법 | AGENTS.md로 표준 규칙 파일 정의하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=LHx_EFoLonQ&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=LHx_EFoLonQ&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=LHx_EFoLonQ&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ujfmp/dJMb9dHmPP0/QwQlzGRSfg3SoJU8QKhYj1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=18_282_224_506,https://scrap.kakaocdn.net/dn/bEkPSo/dJMb9c9wCFP/NFK0l4JAvUaFSx4Eg8Z751/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=18_282_224_506,https://scrap.kakaocdn.net/dn/eiafcX/dJMb9iaPTYE/wNY423KFV2pC1X5qdRpUj1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=18_282_224_506&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;코딩 에이전트 성능을 20배 올리는 가장 쉬운 방법 | AGENTS.md로 표준 규칙 파일 정의하기&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/LHx_EFoLonQ&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;</description>
      <category>Projects/block-server</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/541</guid>
      <comments>https://annovation.tistory.com/541#entry541comment</comments>
      <pubDate>Mon, 16 Mar 2026 16:00:05 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (3) Redis Distributed Lock</title>
      <link>https://annovation.tistory.com/540</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;Redis Distributed Lock&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Redis에서 제공하는 대표적인 라이브러리 Lettuce 와 Reddison 을 활용하여 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Lettuce&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;170&quot; data-end=&quot;233&quot;&gt;비동기(Async) 및 동기(Sync) 방식 모두 지원하는 Redis 클라이언트이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️Redis 클라이언트 란?&lt;br /&gt;애플리케이션(Spring, Node 등)이 Redis 서버와 통신하기 위해 사용하는 라이브러리&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;234&quot; data-end=&quot;276&quot;&gt;Netty 기반의 비동기 I/O 처리를 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️Netty 란?&lt;br /&gt;Lettuce가 Redis 서버와 통신할 때 사용하는 비동기 내부 네트워크 프레임워크&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;277&quot; data-end=&quot;316&quot;&gt;Lettuce는 분산락을 제공하는 라이브러리가 아니라 Redis 명령을 실행하는 클라이언트이기 때문에 lock을 Redis 명령어로 직접 구현해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️setnx 명령어를 활용하여 분산락 구현&lt;br /&gt;setnx :&amp;nbsp;Set If Not Exist의 줄임말로, 특정 key에 value 값이 존재하지 않을 경우에 값을 설정(set) 하는 명령어&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;277&quot; data-end=&quot;316&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;spin Lock 방식&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- lock을 획득하려는 스레드가 lock을 사용할 수 있는지 반복적으로 확인하면서 lock을 시도하는 방식&lt;br /&gt;- retry 로직을 작성해야한다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;277&quot; data-end=&quot;316&quot;&gt;Spring Data Redis 라이브러리에서 기본적으로 사용 가능하다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;277&quot; data-end=&quot;316&quot;&gt;Spring Boot 2.x 이상에서는 기본 Redis 클라이언트로 Lettuce가 사용된다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;277&quot; data-end=&quot;316&quot;&gt;Redis 명령을 이용한 간단한 분산 락 구현은 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-end=&quot;316&quot; data-start=&quot;277&quot;&gt;Lettuce는 Redis Cluster 환경에서도 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;❗️Redis 클러스터 환경(cluster environment) 이란? (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/&quot;&gt;공식 문서&lt;/a&gt;)&lt;br /&gt;여러 개의 Redis 서버를 묶어서 하나의 Redis처럼 사용하는 구조&lt;br /&gt;데이터를 여러 서버에 나눠 저장(sharding) 하는 것&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 단점 (&lt;a href=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8CSynchronized%20%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-start=&quot;342&quot; data-end=&quot;408&quot;&gt;단순한 분산 락을 구현하는 데 사용할 수 있지만, 완전한 Redisson 수준의 분산 락을 제공하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️분산 락&lt;br /&gt;여러 프로세스가 공유 자원을 '상호 배타적으로' 접근하도록 하는 것 (&lt;a href=&quot;https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;redis 공식 문서&lt;/a&gt;)&lt;br /&gt;즉, 여러 서버나 요청이 같은 데이터를 사용하려고 할 때, 동시에 처리하지 않고 한 요청이 작업을 끝낼 때까지 다른 요청은 기다리게 만드는 것&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 사용 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;infrastructure/redis/RedisLockRepository.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773404290826&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Component
public class RedisLockRepository {
    private RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    public Boolean lock(Long key) {
        return redisTemplate
                .opsForValue()
                .setIfAbsent(generateKey(key), &quot;lock&quot;, Duration.ofMillis(3_000));
    }

    public Boolean unlock(Long key) {
        return redisTemplate.delete(generateKey(key));
    }

    public String generateKey(Long key) {
        return key.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;➡️ lock(Long key) 메서드&lt;/p&gt;
&lt;pre id=&quot;code_1773404698185&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean lock(Long key) {
    return redisTemplate.opsForValue()
        .setIfAbsent(key, &quot;lock&quot;, Duration.ofMillis(3000));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Duration.ofMillis(3_000) : 3초&amp;nbsp;후&amp;nbsp;자동으로&amp;nbsp;lock&amp;nbsp;삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Redis 명령&lt;/p&gt;
&lt;pre id=&quot;code_1773404737365&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET key value NX PX 3000&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;key가 없으면 생성&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;rarr; 락 획득&lt;/li&gt;
&lt;li&gt;key가 있으면 실패 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; 이미 다른 스레드가 락 사용중&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;➡️ unlock(Longe key) 메서드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773404913347&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void unlock(Long key) {
    redisTemplate.delete(key);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 Redis 명령&lt;/p&gt;
&lt;pre id=&quot;code_1773405294610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DEL key&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;lock 삭제 &lt;/span&gt;&lt;span&gt;&amp;rarr; 다른 프로세스가 lock 획득 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Reddison&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 특징&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 단점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 사용 방법&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/540</guid>
      <comments>https://annovation.tistory.com/540#entry540comment</comments>
      <pubDate>Wed, 11 Mar 2026 23:52:22 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (2) Database Lock</title>
      <link>https://annovation.tistory.com/537</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Database Lock&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스가 제공하는 Lock을 활용하여 데이터 정합성을 맞출 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Pessimistic Lock (비관적 락)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;동시성 제어-비관적 락.drawio.png&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvL9Ow/dJMcaa5lklp/CI33kyUxJexkjAbmCxlaK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvL9Ow/dJMcaa5lklp/CI33kyUxJexkjAbmCxlaK1/img.png&quot; data-alt=&quot;출처 : https://minjooig.tistory.com/147#비관적%20락(Perssimistic%20Locking)이란%3F-1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvL9Ow/dJMcaa5lklp/CI33kyUxJexkjAbmCxlaK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvL9Ow%2FdJMcaa5lklp%2FCI33kyUxJexkjAbmCxlaK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;409&quot; data-filename=&quot;동시성 제어-비관적 락.drawio.png&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://minjooig.tistory.com/147#비관적%20락(Perssimistic%20Locking)이란%3F-1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실제 데이터에 직접적으로 Lock&lt;/b&gt;을 거는 방식&lt;/li&gt;
&lt;li&gt;다른 트랜잭션은 Lock이 해제되기 전까지 데이터를 읽을 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) 특징 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-1-pessimistic-lock%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 충돌을 피하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;먼저 Lock을 걸고 작업을 진행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;일반적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;Exclusive Lock(X Lock, 배타적 락)을 사용해 다른 트랜잭션의 Read/Write을 방지한다.&lt;/li&gt;
&lt;li&gt;은행 계좌, 항공권 좌석, 콘서트 티켓 등&amp;nbsp;충돌이 빈번하고 데이터 정합성이 중요한 환경에서 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 장점 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#2-database-lock&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;충돌이 빈번하게 일어날 경우 Optimistic Lock보다 성능이 좋을 수 있다.&lt;/li&gt;
&lt;li&gt;Lock을 통해 update를 제어하기 때문에 데이터의 정합성이 보장된다.&lt;br /&gt;(데이터를 수정할 때 미리 잠가(lock) 버리기 때문에, 여러 사람이 동시에 건드려도 데이터가 꼬이지 않는다&lt;span&gt;는 의미)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3) 단점 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.youtube.com/watch?v=oJrVl6QKzHw&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 다른 자원을 쥐고 놓지 않는 데드락(Deadlock) 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4) 사용 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Data JPA에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Lock&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션을 사용하여 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772808412744&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface StockRepository extends JpaRepository&amp;lt;Stock, Long&amp;gt; {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select s from Stock s where s.id = :id&quot;)
    Stock findByIdWithPessimistic(Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;➡️&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;@Lock(LockModeType.PESSIMISTIC_WRITE)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;해당 데이터에 대해 쓰기 잠금을 설정하여, 다른 트랜잭션이 해당 데이터를 읽거나 수정하지 못하게 막는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Optimistic Lock (낙관적 락)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;동시성 제어.drawio.png&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pyX48/dJMcab4eUDR/6zNhItgDLKFkj04pyZct4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pyX48/dJMcab4eUDR/6zNhItgDLKFkj04pyZct4k/img.png&quot; data-alt=&quot;출처 : https://minjooig.tistory.com/147#비관적%20락(Perssimistic%20Locking)이란%3F-1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pyX48/dJMcab4eUDR/6zNhItgDLKFkj04pyZct4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpyX48%2FdJMcab4eUDR%2F6zNhItgDLKFkj04pyZct4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;592&quot; height=&quot;392&quot; data-filename=&quot;동시성 제어.drawio.png&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://minjooig.tistory.com/147#비관적%20락(Perssimistic%20Locking)이란%3F-1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 읽고 수정할 때까지 락을 걸지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수정 시점에서 충돌 여부를 확인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제로 락을 이용하지 않고 version 을 이용&lt;/b&gt;해 데이터 정합성을 맞춤&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) 특징 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-3-named-lock%EB%84%A4%EC%9E%84%EB%93%9C-%EB%9D%BD&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 데이터를 읽은 후에 update를 할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내가 읽은 버전이 맞는 확인&lt;/b&gt;하며 업데이트한다.&lt;/li&gt;
&lt;li&gt;일반적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;버전번호&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;타임스탬프를 활용해 변경 여부를 확인&lt;/li&gt;
&lt;li&gt;동시성이 높은 환경에서 유용하며, 충돌 가능성이 낮은 경우 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 장점 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#named-lock&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 Lock을 잡지 않아 Pessimistic Lock보다 성능상으로 이점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3) 단점 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-3-named-lock%EB%84%A4%EC%9E%84%EB%93%9C-%EB%9D%BD&quot;&gt;참고 자료 1&lt;/a&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#named-lock&quot;&gt;참고 자료 2&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;충돌이 발생하면 다시 데이터를 읽고 재시도해야 한다.&lt;/li&gt;
&lt;li&gt;update가 실패했을 때 재시도 로직을 개발자가 직접 작성해야 하는 번거로움이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3-1) 예외처리를 하는 이유 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8CSynchronized%20%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;사용자 A가 Stock 데이터를 조회 (SELECT)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id=1, quantity=100, version=1&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;사용자 B가 동일한 Stock 데이터를 조회 (SELECT)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;, quantity=&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;, version=&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;1&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;사용자 A가 Stock을 업데이트 (UPDATE)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772808412749&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;UPDATE stock SET quantity=90, version=2 WHERE id=1 AND version=1;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-end=&quot;1198&quot; data-start=&quot;1167&quot;&gt;성공적으로 업데이트됨 (version=2로 증가)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;사용자 B가 Stock을 업데이트 시도 (UPDATE)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772808412750&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;UPDATE stock SET quantity=80, version=2 WHERE id=1 AND version=1;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-end=&quot;1392&quot; data-start=&quot;1327&quot;&gt;version=1을 가진 데이터가 없으므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;예외 발생&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(OptimisticLockException)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4) 사용 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stock Entity Class 에 version 컬럼 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772808412751&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private Long productId;

  private Long quantity;

  @Version
  private Long version;
  
  //생략...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;➡️ 주의 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;javax.persistence&lt;span&gt;&amp;nbsp;&lt;/span&gt;패키지에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Version 어노테이션을 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Repository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772808412752&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface StockRepository extends JpaRepository&amp;lt;Stock, Long&amp;gt; {

  @Lock(LockModeType.OPTIMISTIC)
  @Query(&quot; select s from Stock s where s.id = :id &quot;)
  Stock findByIdWithOptimisticLock(Long id);
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;➡️ Spring Data JPA에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Lock&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션을 사용하여 구현할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;facade 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772808412753&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class OptimisticLockStockFacade {

    private final OptimisticLockStockService optimisticLockStockService;

    public OptimisticLockStockFacade(OptimisticLockStockService optimisticLockStockService){
        this.optimisticLockStockService = optimisticLockStockService;
    }

    public void decrease(Long id, Long quantity) throws InterruptedException{
        while (true) {  //업데이트를 실패했을 때 재시도를 해야함
            try {
                optimisticLockStockService.decrease(id, quantity);

                //정상적으로 업데이트 되면 빠져나감
                break;
            } catch (Exception e) {
                //수량 감소 실패 시, 재시도
                Thread.sleep(50);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;➡️ Optimistic Lock은 실패했을 경우 재시도를 해야하기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;facade(퍼사드)와 같은 재시도 로직을 생성해야한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Named Lock&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1-1) 특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름을 가진 메타데이터를 사용하는&amp;nbsp; metadata locking 방법이다. (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://dodop-blog.tistory.com/463&quot;&gt;참고 자료&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션이 이 lock을 획득할 수 없도록 한다. (&lt;a href=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8CSynchronized%20%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 자료&lt;/a&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;트랜잭션이 종료 될 때 Lock이 자동으로 해제되지 않으므로 별도의 명령어로 해제가 필요하다. (&lt;a style=&quot;color: #0070d1; text-align: left;&quot; href=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8CSynchronized%20%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1&quot;&gt;참고 자료&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;MySQL에서는 GET_LOCK() 함수를 사용해서 &lt;b&gt;사용자가 정의한 이름(name)을 기준으로 lock을 획득&lt;/b&gt;할 수 있다. (&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;1-2) Named Lock 데이터 적용 과정 (&lt;a href=&quot;https://velog.io/@kdmin0706/Java-Spring-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#3-named-lock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 자료&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;651&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn12k3/dJMcaaqWexn/pYLMGqrNzbduJsdGfEJxWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn12k3/dJMcaaqWexn/pYLMGqrNzbduJsdGfEJxWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn12k3/dJMcaaqWexn/pYLMGqrNzbduJsdGfEJxWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn12k3%2FdJMcaaqWexn%2FpYLMGqrNzbduJsdGfEJxWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;651&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;651&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Named Lock은 DB에 락을 걸지 않고, 별도의 공간에 락을 건다.&lt;/li&gt;
&lt;li&gt;session-1이 1이라는 이름으로 락을 건다면 session-1이 1을 해지한 후에 락을 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 장점&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3) 단점 (&lt;a href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-3-named-lock%EB%84%A4%EC%9E%84%EB%93%9C-%EB%9D%BD&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락을 획득하지 못하면 대기하거나 재시도 로직 필요&lt;/li&gt;
&lt;li&gt;트랜잭션 종료 시 자동으로 Lock 해제 되지 않아 Lock을 걸고 해제하는 별도 로직이 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4) 사용 방법 (&lt;a href=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#named-lock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 자료&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Named Lock 을 수행하는 repository 를 생성 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772975646893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface LockRepository extends JpaRepository&amp;lt;Stock, Long&amp;gt; {
    @Query(value = &quot;select get_lock(:key, 3000)&quot;, nativeQuery = true)
    void getLock(String key);

    @Query(value = &quot;select release_lock(:key)&quot;, nativeQuery = true)
    void releaseLock(String key);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lock 획득 해제하는 별도의 명령을 위해 Facade 패턴을 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772977912672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class NamedLockStockFacade {

    private final LockRepository lockRepository;
    private final StockService stockService;

    public NamedLockStockFacade(LockRepository lockRepository, StockService stockService){
        this.lockRepository = lockRepository;
        this.stockService = stockService;
    }

    //decrease 메서드
    @Transactional
    public void decrease(Long id, Long quantity){
        try {
            //lock 획득
            lockRepository.getLock(id.toString());
            //재고 감소
            stockService.decrease(id, quantity);
        }finally {
            //모든 로직이 종료되었을 때, lock 해제
            lockRepository.releaseLock(id.toString());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockService propagation 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772977951562&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized void decrease(Long id, Long quantity){
    // Stock 조회
    Stock stock = stockRepository.findById(id).orElseThrow();

    // 재고를 감소
    stock.decrease(quantity);

    // 갱신된 값을 저장
    stockRepository.saveAndFlush(stock);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 부모의 트랜잭션과 별도로 실행되어야 하기 때문에 propagation을 변경해주어야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️Propagation(전파) 란?&lt;br /&gt;&lt;br /&gt;현재 메서드가 트랜잭션을 어떻게 사용할지 결정하는 규칙&lt;br /&gt;&lt;br /&gt;ex.&lt;br /&gt;@Transactional(propagation = Propagation.REQUIRED) -&amp;gt; 기존 트랜잭션 사용, 없으면 새로 생성&lt;br /&gt;@Transactional(propagation = Propagation.REQUIRES_NEW) -&amp;gt; 항상 새로운 트랜잭션 생성, 기존 트랜잭션은 일시 정지&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;connection pool size 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 데이터 소스를 사용하기 위해 connection pool size를 변경합니다.&lt;br /&gt;maximum-pool-size: 40&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 블로그 : SpringBoot 동시성 이슈 해결방법[3] - Lock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-1-pessimistic-lock%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@kiteof_park/SpringBoot-pessimistic-lock&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772809038307&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SpringBoot 동시성 이슈 해결방법[3] - Lock&quot; data-og-description=&quot;synchronized의 한계와 Lock의 종류 - Pessimistic Lock, Optimistic Lock, Named Lock&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-1-pessimistic-lock%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD&quot; data-og-url=&quot;https://velog.io/@kiteof_park/SpringBoot-동시성-이슈-해결방법3-Lock&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zN0yl/dJMb9kmaHZn/PqKV5XjdiWisXerXkLOYS1/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/bSOdA0/dJMb9gxjjBG/kvMWX5rSR1mLedtDUoaplk/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/rVmIR/dJMb9b3PYCR/QgoLb62ZUwSpHueqOI8Zzk/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398&quot;&gt;&lt;a href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-1-pessimistic-lock%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%953-Lock#-1-pessimistic-lock%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zN0yl/dJMb9kmaHZn/PqKV5XjdiWisXerXkLOYS1/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/bSOdA0/dJMb9gxjjBG/kvMWX5rSR1mLedtDUoaplk/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/rVmIR/dJMb9b3PYCR/QgoLb62ZUwSpHueqOI8Zzk/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SpringBoot 동시성 이슈 해결방법[3] - Lock&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;synchronized의 한계와 Lock의 종류 - Pessimistic Lock, Optimistic Lock, Named Lock&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 블로그 : [Java/Spring] 재고 시스템으로 알아보는 동시성 이슈와 해결 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#2-database-lock&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#2-database-lock&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772809181158&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java/Spring] 재고 시스템으로 알아보는 동시성 이슈와 해결 방법&quot; data-og-description=&quot;인프런 재고시스템으로 알아보는 동시성이슈 해결방법 강의를 듣고 작성한 글입니다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#2-database-lock&quot; data-og-url=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jZi19/dJMb82MA0wX/joVcKDaFSQCSK10Y6aUHU0/img.jpg?width=768&amp;amp;height=402&amp;amp;face=0_0_768_402,https://scrap.kakaocdn.net/dn/cPxk2F/dJMb88eYzAD/YzjiWNHQ23h3sdPWTUJh7K/img.jpg?width=768&amp;amp;height=402&amp;amp;face=0_0_768_402,https://scrap.kakaocdn.net/dn/eGfu3/dJMb83SgJYf/Gr10sSlSAsWCpyIM4ujd70/img.png?width=1812&amp;amp;height=490&amp;amp;face=0_0_1812_490&quot;&gt;&lt;a href=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#2-database-lock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#2-database-lock&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jZi19/dJMb82MA0wX/joVcKDaFSQCSK10Y6aUHU0/img.jpg?width=768&amp;amp;height=402&amp;amp;face=0_0_768_402,https://scrap.kakaocdn.net/dn/cPxk2F/dJMb88eYzAD/YzjiWNHQ23h3sdPWTUJh7K/img.jpg?width=768&amp;amp;height=402&amp;amp;face=0_0_768_402,https://scrap.kakaocdn.net/dn/eGfu3/dJMb83SgJYf/Gr10sSlSAsWCpyIM4ujd70/img.png?width=1812&amp;amp;height=490&amp;amp;face=0_0_1812_490');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java/Spring] 재고 시스템으로 알아보는 동시성 이슈와 해결 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인프런 재고시스템으로 알아보는 동시성이슈 해결방법 강의를 듣고 작성한 글입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 블로그 : [Java/Spring] 재고시스템으로 알아보는 동시성 이슈 해결 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8COptimistic%20Lock(%EB%82%99%EA%B4%80%EC%A0%81%20%EB%9D%BD)%20%ED%99%9C%EC%9A%A9-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://girokeulhaja.tistory.com/107&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772809362275&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java/Spring] 재고시스템으로 알아보는 동시성 이슈 해결 방법 &quot; data-og-description=&quot;❗해당 포스팅은 인프런에서 제공해 주는 강의 내용을 개인적으로 정리하였음을 알려드립니다.&amp;nbsp;재고시스템으로 알아보는 동시성이슈 해결방법 강의 | 최상용 - 인프런최상용 | , 동시성 이슈 &quot; data-og-host=&quot;girokeulhaja.tistory.com&quot; data-og-source-url=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8COptimistic%20Lock(%EB%82%99%EA%B4%80%EC%A0%81%20%EB%9D%BD)%20%ED%99%9C%EC%9A%A9-1&quot; data-og-url=&quot;https://girokeulhaja.tistory.com/107&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cVWFoe/dJMb8ZvznJQ/mDK1XioKKCrpxgab6CYO40/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/z2NDZ/dJMb8WexyKO/MHrm0kXuWtKaCythMdpT10/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/QjafP/dJMb8YXJpSN/SrjBUgvnFu9qeeOEAxjaKk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080&quot;&gt;&lt;a href=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8COptimistic%20Lock(%EB%82%99%EA%B4%80%EC%A0%81%20%EB%9D%BD)%20%ED%99%9C%EC%9A%A9-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://girokeulhaja.tistory.com/107#%F0%9F%93%8COptimistic%20Lock(%EB%82%99%EA%B4%80%EC%A0%81%20%EB%9D%BD)%20%ED%99%9C%EC%9A%A9-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cVWFoe/dJMb8ZvznJQ/mDK1XioKKCrpxgab6CYO40/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/z2NDZ/dJMb8WexyKO/MHrm0kXuWtKaCythMdpT10/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/QjafP/dJMb8YXJpSN/SrjBUgvnFu9qeeOEAxjaKk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java/Spring] 재고시스템으로 알아보는 동시성 이슈 해결 방법 &lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;❗해당 포스팅은 인프런에서 제공해 주는 강의 내용을 개인적으로 정리하였음을 알려드립니다.&amp;nbsp;재고시스템으로 알아보는 동시성이슈 해결방법 강의 | 최상용 - 인프런최상용 | , 동시성 이슈&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;girokeulhaja.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 유튜브 : &lt;span&gt;락의 두 종류: 비관적 락 vs 낙관적 락&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=oJrVl6QKzHw&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=oJrVl6QKzHw&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=oJrVl6QKzHw&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/cNmSoQ/dJMb8WexyKa/iMiIXyUlXkH0T3iJ75P9m1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bSpl1n/dJMb8TB7v9N/kidMd61NOBGTPoyXy7t2Zk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/sHFvg/dJMb8SpFSay/FlPz31vHv5et5vZ9m878AK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;락의 두 종류: 비관적 락 vs 낙관적 락 ＃개발자개념장착&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/oJrVl6QKzHw&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) 블로그 : 인프런) 재고시스템으로 알아보는 동시성 이슈 해결 (1)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dodop-blog.tistory.com/463&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dodop-blog.tistory.com/463&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772974515074&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;인프런) 재고시스템으로 알아보는 동시성 이슈 해결 (1)&quot; data-og-description=&quot;동시성 이슈 해결을 위한 인프런 강의를 듣고 실습해보았다.&amp;nbsp;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&amp;nbsp;재고시스템으로 알아보는 동&quot; data-og-host=&quot;dodop-blog.tistory.com&quot; data-og-source-url=&quot;https://dodop-blog.tistory.com/463&quot; data-og-url=&quot;https://dodop-blog.tistory.com/463&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/burOuT/dJMb87f4ufX/FYkbYhnlAf2HHddwMvpPeK/img.png?width=800&amp;amp;height=353&amp;amp;face=0_0_800_353,https://scrap.kakaocdn.net/dn/bADN4d/dJMb9lk5b39/rAGAaaob4PA8s8gCeBY131/img.png?width=800&amp;amp;height=353&amp;amp;face=0_0_800_353,https://scrap.kakaocdn.net/dn/kSe7t/dJMb9jOk0xf/essLrohcJClWvAhX6lOllk/img.png?width=2398&amp;amp;height=1220&amp;amp;face=0_0_2398_1220&quot;&gt;&lt;a href=&quot;https://dodop-blog.tistory.com/463&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dodop-blog.tistory.com/463&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/burOuT/dJMb87f4ufX/FYkbYhnlAf2HHddwMvpPeK/img.png?width=800&amp;amp;height=353&amp;amp;face=0_0_800_353,https://scrap.kakaocdn.net/dn/bADN4d/dJMb9lk5b39/rAGAaaob4PA8s8gCeBY131/img.png?width=800&amp;amp;height=353&amp;amp;face=0_0_800_353,https://scrap.kakaocdn.net/dn/kSe7t/dJMb9jOk0xf/essLrohcJClWvAhX6lOllk/img.png?width=2398&amp;amp;height=1220&amp;amp;face=0_0_2398_1220');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;인프런) 재고시스템으로 알아보는 동시성 이슈 해결 (1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;동시성 이슈 해결을 위한 인프런 강의를 듣고 실습해보았다.&amp;nbsp;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&amp;nbsp;재고시스템으로 알아보는 동&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dodop-blog.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6) MySQL Docs : Locking Functions&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772977048753&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL :: MySQL 8.0 Reference Manual :: 14.14 Locking Functions&quot; data-og-description=&quot;This section describes functions used to manipulate user-level locks. Table&amp;nbsp;14.19&amp;nbsp;Locking Functions GET_LOCK(str,timeout) Tries to obtain a lock with a name given by the string str, using a timeout of timeout seconds. A negative timeout value means infin&quot; data-og-host=&quot;dev.mysql.com&quot; data-og-source-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&quot; data-og-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL :: MySQL 8.0 Reference Manual :: 14.14 Locking Functions&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This section describes functions used to manipulate user-level locks. Table&amp;nbsp;14.19&amp;nbsp;Locking Functions GET_LOCK(str,timeout) Tries to obtain a lock with a name given by the string str, using a timeout of timeout seconds. A negative timeout value means infin&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.mysql.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/537</guid>
      <comments>https://annovation.tistory.com/537#entry537comment</comments>
      <pubDate>Mon, 9 Mar 2026 03:09:41 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (1) Synchronized</title>
      <link>https://annovation.tistory.com/536</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;동시성&amp;nbsp;문제&amp;nbsp;(Race&amp;nbsp;Condition)&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레이스 컨디션을 해결하는 방법에는 크게 3가지 관점이 있다.&lt;/li&gt;
&lt;li&gt;첫번째는 Java에서 지원하는 Synchronized 방법으로 해결하기&lt;/li&gt;
&lt;li&gt;두번째는 DB가 제공하는 Lock 을 이용하여 데이터 정합성 맞추기&lt;/li&gt;
&lt;li&gt;세번째는 Redis 를 활용하여 대표적인 라이브러리 Lettuce 와 Ressison 으로 해결하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;동시성 문제 (Race Condition) 이란?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/508&quot;&gt;https://annovation.tistory.com/508&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772788179007&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[동시성 처리] Java 에서 발생하는 동시성 문제란?&quot; data-og-description=&quot;Java 동시성 문제 (Race Condition, 레이스 컨디션) Java 동시성 문제란?Java에서 여러 스레드가 같은 데이터(필드/객체 상태) 를 공유하며 실행될 때, 올바른 제어 없이 접근하면 오류가 생긴다.이렇게&quot; data-og-host=&quot;annovation.tistory.com&quot; data-og-source-url=&quot;https://annovation.tistory.com/508&quot; data-og-url=&quot;https://annovation.tistory.com/508&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cDeAXH/dJMb8866KwP/16UyKvzOjY4StBiA5kaMw1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bSw4Ph/dJMb85vMPna/JuHUjAJkYcvlTv8zFFkhWk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/4cfsv/dJMb86nVq1P/0y7h6uYYA8KHVxxKOd7i70/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/508&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://annovation.tistory.com/508&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cDeAXH/dJMb8866KwP/16UyKvzOjY4StBiA5kaMw1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bSw4Ph/dJMb85vMPna/JuHUjAJkYcvlTv8zFFkhWk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/4cfsv/dJMb86nVq1P/0y7h6uYYA8KHVxxKOd7i70/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[동시성 처리] Java 에서 발생하는 동시성 문제란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Java 동시성 문제 (Race Condition, 레이스 컨디션) Java 동시성 문제란?Java에서 여러 스레드가 같은 데이터(필드/객체 상태) 를 공유하며 실행될 때, 올바른 제어 없이 접근하면 오류가 생긴다.이렇게&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;annovation.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Application Level&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java에서 제공하는 Synchronized 키워드를 사용하여 한 개의 스레드만 접근 가능하도록 제한할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Synchronized&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772788175325&quot; class=&quot;gradle&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;//@Transactional
public synchronized void decrease(Long id, Long quantity) {
    // Stock 조회
    // 재고를 감소시킨 뒤
    // 갱신된 값을 저장
    Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);

    stockRepository.save(stock);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized&lt;span&gt;&amp;nbsp;&lt;/span&gt;키워드를 메서드 선언부에 붙여, 해당 메서드는 하나의 스레드만 접근 가능하도록 제한한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✅ @Transactional 을 주석처리하는 이유 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot;&gt;출처&lt;/a&gt;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@Transactional의 동작 원리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional은 Spring AOP 기반 프록시(proxy)를 통해 트랜잭션 경계를 생성한다. (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot;&gt;공식 문서&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;Spring uses AOP proxies to apply transactional behavior.&lt;/blockquote&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;❗️개념 정리&lt;br /&gt;&lt;br /&gt;1) Spring AOP(Aspect Oriented Programming) 란? (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.spring.io/spring-framework/reference/core/aop.html&quot;&gt;공식 문서&lt;/a&gt;)&lt;br /&gt;- 관점 지향 프로그래밍이란 뜻으로, 공통 기능을 비즈니스 로직에서 분리해서 자동으로 적용하는 기술이다.&lt;br /&gt;ex.&lt;br /&gt;공통 기능 (ex. 로그)&lt;br /&gt;&amp;darr;&lt;br /&gt;비즈니스 로직 실행&lt;br /&gt;// Spring은 실제 객체 대신 Proxy 객체를 먼저 호출하고, Proxy가 공통 기능을 실행한 뒤 실제 비즈니스 로직을 호출한다.&lt;br /&gt;&amp;darr;&lt;br /&gt;공통 기능 (ex. 로그)&lt;br /&gt;&lt;br /&gt;2) 프록시(proxy) 란?&lt;br /&gt;- Spring 에서 AOP 개념을 구현한 기능으로, 원래 객체 대신 호출을 받아서 중간에서 처리하는 객체&lt;br /&gt;ex. 원래 객체 UserService를 호출했다면, Spring이 만든 UserServiceProxy가 호출된다.&lt;br /&gt;&lt;br /&gt;3) 트랜잭션 이란?&lt;br /&gt;- 여러 데이터베이스 작업(DB 안의 데이터를 읽거나 변경하는 모든 행위, CRUD)을 하나의 논리적 작업 단위로 묶는 것이다.&lt;br /&gt;- 즉 여러 쿼리(데이터베이스에게 요청을 보내는 명령문)가 실행되더라도 전부 성공하거나 / 전부 실패해야 한다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #353638;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Spring이 해당 메서드를 감싼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;프록시(proxy) 객체를 생성해 트랜잭션을 관리&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;한다.&lt;br /&gt;예를 들면, StockService 를 필드로 가지는 클래스를 새로 만들어서 실행 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot;&gt;참고자료&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Proxy 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772788175326&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderServiceProxy {

    OrderService target;

    public void order() {

        System.out.println(&quot;로그 시작&quot;);

        target.order();

        System.out.println(&quot;로그 종료&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #353638;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;실행 흐름&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772788175326&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
 &amp;darr;
Proxy.order()
 &amp;darr;
OrderService.order()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@Transactional 을 주석처리하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #353638;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;commit(트랜잭션의 모든 작업을 최종 확정해 DB에 영구 저장하는 것)은 트랜잭션 완료 시점에 이루어진다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #353638;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;commit 전에 DB에 기록되지만, 다른 트랜잭션에서는 그 변경사항을 볼 수 없다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #353638;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;따라서 재고 감소 메서드 decrease()가 완료되고 실제 DB에 반영 전에 다른 스레드가 decrease()를 호출하면, 다른 스레드는 갱신되기 전에 값을 가져가 이전과 동일한 문제가 발생한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #353638;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;즉, 현재 테스트 코드에서는 여러 스레드가 동시에 실행되지만, 하나의 트랜잭션 안에서 commit이 되지 않은채 데이터에 접근하는 구조이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;synchronized 동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized&lt;span&gt;&amp;nbsp;&lt;/span&gt;키워드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;같은 인스턴스에서 실행되는 여러 스레드&lt;/b&gt;가&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나씩 순차적으로 메서드를 실행하도록 보장한다.&lt;/li&gt;
&lt;li&gt;같은 인스턴스에서 적용되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;synchronized Lock 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772788175328&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;StockService service = new StockService();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 스레드&lt;/p&gt;
&lt;pre id=&quot;code_1772788175328&quot; class=&quot;autohotkey&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Thread A &amp;rarr; service.decrease()
Thread B &amp;rarr; service.decrease()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우&lt;/p&gt;
&lt;pre id=&quot;code_1772788175328&quot; class=&quot;autohotkey&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Thread A &amp;rarr; lock(service)
Thread B &amp;rarr; 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre id=&quot;code_1772788175328&quot; class=&quot;properties&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;A 실행
B 실행&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 인스턴스에서 적용되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;synchronized Lock 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772788175328&quot; class=&quot;haxe&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;StockService service1 = new StockService();
StockService service2 = new StockService();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 스레드&lt;/p&gt;
&lt;pre id=&quot;code_1772788175328&quot; class=&quot;autohotkey&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Thread A &amp;rarr; service1.decrease()
Thread B &amp;rarr; service2.decrease()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우&lt;/p&gt;
&lt;pre id=&quot;code_1772788175328&quot; class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;lock(service1)
lock(service2)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre id=&quot;code_1772788175329&quot; class=&quot;&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;동시에 실행&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized는 객체 기준으로 Lock을 잡는데 Spring이 Proxy를 사용하면 Lock이&amp;nbsp;걸리는&amp;nbsp;객체와&amp;nbsp;호출&amp;nbsp;객체가&amp;nbsp;달라질&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional이 적용되면 Spring은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원본 StockService 대신 프록시 객체를 통해 메서드를 실행&lt;/b&gt;하여 트랜잭션을 관리한다.&lt;/li&gt;
&lt;li&gt;하지만 synchronized는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원본 객체 인스턴스를 기준으로 동기화&lt;/b&gt;되기 때문에, 프록시가 개입하는 구조에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의도한 대로 동기화가 보장되지 않을 가능성이 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✅ synchronized 한계 (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot;&gt;출처&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized는 하나의 프로세스 안에서만 동기화를 보장한다.&lt;/li&gt;
&lt;li&gt;서버가 1대일 때는 데이터 접근을 서버 1대만 해서 괜찮겠지만,&lt;br /&gt;서버가 2대 혹은 그 이상인 경우 데이터 접근을 여러 곳에서 할 수 있게 된다.&lt;/li&gt;
&lt;li&gt;실제 운영 서비스는 대부분 2대 이상의 서버를 사용하므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;synchronized&lt;span&gt;&amp;nbsp;&lt;/span&gt;거의 사용❌&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Spring Docs : Using @Transactional&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772788304513&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Using @Transactional :: Spring Framework&quot; data-og-description=&quot;The @Transactional annotation is metadata that specifies that an interface, class, or method must have transactional semantics (for example, &amp;quot;start a brand new read-only transaction when this method is invoked, suspending any existing transaction&amp;quot;). The de&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Using @Transactional :: Spring Framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The @Transactional annotation is metadata that specifies that an interface, class, or method must have transactional semantics (for example, &quot;start a brand new read-only transaction when this method is invoked, suspending any existing transaction&quot;). The de&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 블로그 : 동시성 이슈 해결방법[2] - synchronized&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@kiteof_park/SpringBoot-java-synchronized&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772788272670&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;동시성 이슈 해결방법[2] - synchronized&quot; data-og-description=&quot;synchronized로 동시성 이슈를 해결하지 못하는 이유와 @Transactional과의 충돌!&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot; data-og-url=&quot;https://velog.io/@kiteof_park/SpringBoot-동시성-이슈-해결방법2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sNrCo/dJMb9fZtdB7/H8Y56dkpz8jKEzNA4oWFYK/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/cSA7NY/dJMb9kmaGtJ/iy19VlE8KM8GbqRi00EhG0/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/yQD10/dJMb9kmaGtI/KOHNDhkBHXMpzoxniUxnGk/img.png?width=1024&amp;amp;height=435&amp;amp;face=0_0_1024_435&quot;&gt;&lt;a href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sNrCo/dJMb9fZtdB7/H8Y56dkpz8jKEzNA4oWFYK/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/cSA7NY/dJMb9kmaGtJ/iy19VlE8KM8GbqRi00EhG0/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/yQD10/dJMb9kmaGtI/KOHNDhkBHXMpzoxniUxnGk/img.png?width=1024&amp;amp;height=435&amp;amp;face=0_0_1024_435');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;동시성 이슈 해결방법[2] - synchronized&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;synchronized로 동시성 이슈를 해결하지 못하는 이유와 @Transactional과의 충돌!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/536</guid>
      <comments>https://annovation.tistory.com/536#entry536comment</comments>
      <pubDate>Fri, 6 Mar 2026 18:12:12 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Redis 실습 2</title>
      <link>https://annovation.tistory.com/535</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Spring Boot에서 Redis 사용해보기&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Redis의 Hash 자료구조로 저장되는 엔티티(Entity) 만들기&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;엔티티(Entity) : 저장소(DB, Redis 등)에 저장되는 도메인 객체(클래스)&lt;br /&gt;도메인 객체(Domain Object) : 서비스가 다루는 현실 세계의 개념(ex. 상품, 주문, 계좌 등) 들을 코드로 표현한 객체&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1) 요구사항&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아래와 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;도메인 객체의 속성(필드)을 가지는 주문 ID, 판매 물품, 갯수, 총액, 결제 여부에 대한 데이터를 지정하기 위한 ItemOrder 클래스를 RedisHash 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772719852404&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 주문 ID - String
2. 판매 물품 - String
3. 갯수 - Integer
4. 총액 - Long
5. 주문 상태 - String&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 코드 구현&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemOrder.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772720666554&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@RedisHash(&quot;order&quot;)
public class ItemOrder {
    @Id
    private String id;
    private String item;
    private Integer count;
    private Long totalPrice;
    private String status;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ @RedisHash&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이 클래스가 Redis의 Hash 자료구조로 저장되는 엔티티(Entity)라는 것을 Spring에게 알려준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;주문에 대한 CRUD를 진행하는 기능&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 요구사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemOrder의 속성값들을 ID를 제외하고 클라이언트에서 전달해준다. (Spring Boot 서버에서 ID를 생성해야 함)&lt;/li&gt;
&lt;li&gt;성공하면 저장된 ItemOrder를 사용자에게 응답해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 코드 구현 : Spring Data Redis Repository 의 CrudRepository 활용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OrderRepository.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772723104065&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.springframework.data.repository.CrudRepository;

public interface OrderRepository
        extends CrudRepository&amp;lt;ItemOrder, String&amp;gt; {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ CrudRepository&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본적인 CURD 메서드를 제공하는 인터페이스 (save(), findById(), deleteById() ... )&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OrderController.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772723104066&quot; class=&quot;livescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping(&quot;orders&quot;)
public class OrderController {
    private final OrderRepository orderRepository;

    public OrderController(
            OrderRepository orderRepository
    ) {
        this.orderRepository = orderRepository;
    }

    @PostMapping
    public ItemOrder create(
            @RequestBody
            ItemOrder order
    ) {
        return orderRepository.save(order);
    }

    @GetMapping
    public List&amp;lt;ItemOrder&amp;gt; readAll() {
        List&amp;lt;ItemOrder&amp;gt; orders = new ArrayList&amp;lt;&amp;gt;();
        orderRepository.findAll()
                .forEach(orders::add);
        return orders;
    }

    @GetMapping(&quot;{id}&quot;)
    public ItemOrder readOne(
            @PathVariable(&quot;id&quot;)
            String id
    ) {
        return orderRepository.findById(id)
                .orElseThrow(() -&amp;gt; new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @PutMapping(&quot;{id}&quot;)
    public ItemOrder update(
            @PathVariable(&quot;id&quot;)
            String id,
            @RequestBody
            ItemOrder order
    ) {
        ItemOrder target = orderRepository
                .findById(id)
                .orElseThrow(() -&amp;gt; new ResponseStatusException(HttpStatus.NOT_FOUND));
        target.setItem(order.getItem());
        target.setCount(order.getCount());
        target.setTotalPrice(order.getTotalPrice());
        target.setStatus(order.getStatus());
        return orderRepository.save(target);
    }

    @DeleteMapping(&quot;{id}&quot;)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(
            @PathVariable(&quot;id&quot;)
            String id
    ) {
        orderRepository.deleteById(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 코드 구현 : RedisTemplate 을 활용해 직접 Redis 명령을 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RedisConfig.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772724021063&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate&amp;lt;String, Integer&amp;gt; articleTemplate(
            RedisConnectionFactory redisConnectionFactory
    ) {
        RedisTemplate&amp;lt;String, Integer&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(RedisSerializer.string());
        template.setValueSerializer(new GenericToStringSerializer&amp;lt;&amp;gt;(Integer.class));
        return template;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ RedisTemplate Bean 생성하는 코드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-1) 각 코드 의미&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Configuration : Spring&amp;nbsp;Bean&amp;nbsp;설정을&amp;nbsp;하는&amp;nbsp;클래스라는 의미&lt;/li&gt;
&lt;li&gt;@Bean
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;해당 어노테이션이 붙은 메서드가 반환하는 객체를 Spring Bean 으로 등록 한다.&lt;/li&gt;
&lt;li&gt;즉, 위 코드에서는 RedisTemplate 을 Spring 이 관리하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RedisConnectionFactory : Redis 서버와 연결을 만드는 객체&lt;/li&gt;
&lt;li&gt;RedisTemplate&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772724229436&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RedisTemplate&amp;lt;String, Integer&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ Redis&amp;nbsp;명령을&amp;nbsp;실행하는&amp;nbsp;클래스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 예시&lt;/p&gt;
&lt;pre id=&quot;code_1772724258141&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redisTemplate.opsForValue().increment(&quot;article:1&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Serializer 설정 (중요)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772724311067&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;template.setValueSerializer(
    new GenericToStringSerializer&amp;lt;&amp;gt;(Integer.class)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ Redis 는 문자열 기반 저장소이기 때문에 Java 객체를 Redis 저장 가능 형태로 변환 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Serializer 라고 하고 위 코드는 Integer &amp;rarr; String 변환하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-2) ArticleController.java&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RedisConfig로 설정한 RedisTemplate 클래스를 Controller에서 사용하는 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772724552163&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(&quot;articles&quot;)
public class ArticleController {
    private final ValueOperations&amp;lt;String, Integer&amp;gt; ops;

    public ArticleController(
            RedisTemplate&amp;lt;String, Integer&amp;gt; articleTemplate
    ) {
        ops = articleTemplate.opsForValue();
    }

    @GetMapping(&quot;{id}&quot;)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void read(
            @PathVariable(&quot;id&quot;)
            Long id
    ) {
        ops.increment(&quot;articles:%d&quot;.formatted(id));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-6-2-2242dc3ef51481f3a285e1abb7bd48e8&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://teamsparta.notion.site/1-6-2-2242dc3ef51481f3a285e1abb7bd48e8&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772716235467&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;챕터1-6 : 실습2 | Notion&quot; data-og-description=&quot;Spring Boot에서 Redis 사용해보기&quot; data-og-host=&quot;teamsparta.notion.site&quot; data-og-source-url=&quot;https://teamsparta.notion.site/1-6-2-2242dc3ef51481f3a285e1abb7bd48e8&quot; data-og-url=&quot;https://teamsparta.notion.site/1-6-2-2242dc3ef51481f3a285e1abb7bd48e8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dXVVcw/dJMb86OZC8a/ssapD3nY9cU2xmJvzQlcSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dyGHru/dJMb81GU11c/1crlwSUTuYkif93Dpj2gm1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-6-2-2242dc3ef51481f3a285e1abb7bd48e8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://teamsparta.notion.site/1-6-2-2242dc3ef51481f3a285e1abb7bd48e8&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dXVVcw/dJMb86OZC8a/ssapD3nY9cU2xmJvzQlcSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dyGHru/dJMb81GU11c/1crlwSUTuYkif93Dpj2gm1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;챕터1-6 : 실습2 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 Redis 사용해보기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;teamsparta.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/DB</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/535</guid>
      <comments>https://annovation.tistory.com/535#entry535comment</comments>
      <pubDate>Thu, 5 Mar 2026 23:11:58 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Redis 실습 1</title>
      <link>https://annovation.tistory.com/534</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;블로그 별 조회수를 Redis로 확인하기&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;구현 기능&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같은 조건의 '내 블로그 글 별 조회수'를 Redis로 확인하는 기능을 만드려고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772638756503&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 블로그 URL의 PATH는 /articles/{id} 형식이다.
2. 로그인 여부와 상관없이 새로고침 될때마다 조회수가 하나 증가한다.
3. 이를 관리하기 위해 적당한 데이터 타입을 선정하고,
4. 사용자가 임의의 페이지에 접속할 때 실행될 명령을 작성해보자.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Redis 명령어&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Redis의 &lt;/span&gt;String 타입은 숫자 카운터로 사용할 수 있고 값을 증가(INCR) 또는 감소(DECR)시키는 연산을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 특정 게시글 조회수 증가&lt;/p&gt;
&lt;pre id=&quot;code_1772639226440&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INCR articles:{id}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 특정 게시글 조회수 증가 예시&lt;/p&gt;
&lt;pre id=&quot;code_1772639265725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INCR articles:1
INCR articles:2
INCR articles:3&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1번 게시글 조회수 +1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 오늘의 조회수 따로 관리&lt;/p&gt;
&lt;pre id=&quot;code_1772639302055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INCR articles:1:today&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1번 게시글의 오늘 조회수 +1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 하루가 지나면 날짜 키로 변경&lt;/p&gt;
&lt;pre id=&quot;code_1772639357795&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RENAME articles:1:today articles:2026-03-05&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오늘 카운터를 날짜 기준 기록으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Spring 사용 예시&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회수 증가 Service&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772639759534&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ArticleViewService {

    private final StringRedisTemplate redisTemplate;

    public Long increaseView(Long articleId) {

        String key = &quot;articles:&quot; + articleId;

        return redisTemplate
                .opsForValue()
                .increment(key);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;redisTemplate&lt;/b&gt; : Spring이 제공하는 API (RedisTemplate)를 통해 Redis 명령을 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;opsForValue()&lt;/b&gt; : &lt;span&gt;Redis의 &lt;/span&gt;String 자료형을 다루는 API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;increment()&lt;/b&gt; : 내부적으로 Redis 명령 'INCR key'를 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;블로그에 로그인 한 사람들의 조회수와 가장 많은 조회수를 기록한 글을 Redis로 확인하기&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;구현 기능&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같은 조건의 '블로그에 로그인한 사람들의 조회수와 가장 많은 조회수를 기록한 글'을 Redis로 확인하는 기능을 만드려고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772639583836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 블로그 URL의 PATH는 `/articles/{id}` 형식이다.
2. 로그인 한 사람들의 계정은 영문으로만 이뤄져 있다.
3. 이를 관리하기 위해 적당한 데이터 타입을 선정하고,
4. 사용자가 임의의 페이지에 접속할 때 실행될 명령을 작성해보자.
5. 만약 상황에 따라 다른 명령이 실행되어야 한다면, 주석으로 추가해보자.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Redis 명령어&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복을 허용하지 않는 자료형 Set을 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) &lt;span&gt;Set에 값 추가&lt;/span&gt; (중복 허용 안 됨)&lt;/p&gt;
&lt;pre id=&quot;code_1772640503332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SADD articles:1 alex
SADD articles:1 brad
SADD articles:1 chad&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 내부&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772640537477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;articles:1 = {alex, brad, chad}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Set에 들어있는 원소 개수 반환&lt;/p&gt;
&lt;pre id=&quot;code_1772640509174&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SCARD articles:1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772640557536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 중복된 값 저장하는 경우 : SADD 반환값은 0&lt;/p&gt;
&lt;pre id=&quot;code_1772640729514&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SADD articles:1 alex brad chad&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 내부에 alex, brad, chad 가 저장되어 있다면, 반환값은 0이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 따라서 SADD의 결과에 따라 명령어 실행 여부 결정 가능&lt;/p&gt;
&lt;pre id=&quot;code_1772640883232&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# SADD의 결과에 따라 명령 실행
# 0이면 스킵
# 1이면 Sorted Set에 추가

ZADD articles:ranks 1 articles:1

ZINCRBY articles:ranks 1 articles:1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ZADD&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772640937696&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Sorted Set &quot;articles:ranks&quot;에
member = articles:1
score = 1
로 추가&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ZINCRBY&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772640958831&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;articles:1의 score를 1 증가&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 높은 조회수의 글을 보고 싶다면&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772641257785&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ZRANGE articles:ranks 0 0 REV&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Spring 사용 예시&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772641085894&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ArticleLikeService {

    private final StringRedisTemplate redis;

    public ArticleLikeService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    /**
     * userId가 articleId에 좋아요를 누르면:
     * 1) Set에 userId 추가 (중복이면 0)
     * 2) 새로 추가된 경우에만 Sorted Set 랭킹 점수 +1
     */
    public boolean like(Long articleId, String userId) {
        String likeSetKey = &quot;article:&quot; + articleId + &quot;:likes&quot;;     // Set
        String rankKey = &quot;articles:ranks&quot;;                         // ZSet
        String member = &quot;articles:&quot; + articleId;                   // ZSet member

        Long added = redis.opsForSet().add(likeSetKey, userId);    // Redis: SADD
        if (added == null) added = 0L;

        if (added == 1L) {
            redis.opsForZSet().incrementScore(rankKey, member, 1); // Redis: ZINCRBY
            return true; // 처음 좋아요 성공
        }
        return false;     // 이미 좋아요 눌렀음
    }

    public Long likeCount(Long articleId) {
        String likeSetKey = &quot;article:&quot; + articleId + &quot;:likes&quot;;
        return redis.opsForSet().size(likeSetKey);                 // Redis: SCARD
    }

    public Double rankScore(Long articleId) {
        String rankKey = &quot;articles:ranks&quot;;
        String member = &quot;articles:&quot; + articleId;
        return redis.opsForZSet().score(rankKey, member);          // Redis: ZSCORE
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-5-1-2242dc3ef51481478520c62135a0e3c9&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://teamsparta.notion.site/1-5-1-2242dc3ef51481478520c62135a0e3c9&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772631881749&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;챕터1-5 : 실습1 | Notion&quot; data-og-description=&quot;Redis 설치 및 기본 명령어 사용해보기&quot; data-og-host=&quot;teamsparta.notion.site&quot; data-og-source-url=&quot;https://teamsparta.notion.site/1-5-1-2242dc3ef51481478520c62135a0e3c9&quot; data-og-url=&quot;https://teamsparta.notion.site/1-5-1-2242dc3ef51481478520c62135a0e3c9&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/betaFQ/dJMb9aKCQey/QUKni6OE3ne62engVYatg1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dvqMqb/dJMb9fZs2CY/l9Wjs8mTxPL8ekEoM9WF7k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-5-1-2242dc3ef51481478520c62135a0e3c9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://teamsparta.notion.site/1-5-1-2242dc3ef51481478520c62135a0e3c9&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/betaFQ/dJMb9aKCQey/QUKni6OE3ne62engVYatg1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dvqMqb/dJMb9fZs2CY/l9Wjs8mTxPL8ekEoM9WF7k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;챕터1-5 : 실습1 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Redis 설치 및 기본 명령어 사용해보기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;teamsparta.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/DB</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/534</guid>
      <comments>https://annovation.tistory.com/534#entry534comment</comments>
      <pubDate>Wed, 4 Mar 2026 23:54:16 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Spring에서 Redis 사용해보기 : RedisTemplate 써보기</title>
      <link>https://annovation.tistory.com/533</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;RedisTemplate&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;RedisTemplate 이란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Spring Data Redis&lt;/b&gt;&lt;/span&gt; 에서 제공하는 핵심 클래스 (&lt;a href=&quot;https://docs.spring.io/spring-data-redis/reference/api/java/org/springframework/data/redis/core/RedisTemplate.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Key를 직접 설정하고 자료형도 직접 선택해 가면서 Redis를 활용하고 싶다면, RedisTemplate 을 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;RedisTemplate &lt;/span&gt;을 정의하면서 Key와 Value로 사용될 Java 클래스를 정하고, 이후 사용할 세부 작업을 RedisOperations 를 이용해 가져오는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;StringRedisTemplate&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;StringRedisTemplate 이란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RedisTemplate을 상속받은 클래스로, 복잡한 작업없이 Java의 문자열만 다루는 경우, StringRedisTemplate 이 기본으로 만들어진다.&lt;/li&gt;
&lt;li&gt;이는 Key와 Value를 전부 Java의 문자열이라고 가정, 문자열 데이터를 주고받기 위한 작업들을 준비하며, 기본 설정을 가지고 자동으로 만들어져 주입되는 Spring Bean 이다.&lt;/li&gt;
&lt;li&gt;Redis의 String 자료형만 사용하는것이 아닌, Redis와 Spring Boot 사이에 데이터를 주고받는 과정에서 직렬화 - 역직렬화 할때 Java의 String으로 취급되는 클래스이다.&lt;/li&gt;
&lt;li&gt;Redis List에 넣을 때 Java 문자열을 넣고, Redis Set에 넣을 때 Java 문자열을 넣는다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;RedisTemplateTests.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772548645862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
public class RedisTemplateTests {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Test
    public void stringOpsTest() {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772547047283&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;챕터1-4 : Spring에서 Redis 사용해보기 | Notion&quot; data-og-description=&quot;Spring Boot 프로젝트 준비&quot; data-og-host=&quot;teamsparta.notion.site&quot; data-og-source-url=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; data-og-url=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bH3ZKR/dJMb8XkcYXX/8kd85csfqUApyplG1Lk9J0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/XkgBF/dJMb8SpFyZ9/OfTFdk6KHmLbcZqdO37zf1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bH3ZKR/dJMb8XkcYXX/8kd85csfqUApyplG1Lk9J0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/XkgBF/dJMb8SpFyZ9/OfTFdk6KHmLbcZqdO37zf1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;챕터1-4 : Spring에서 Redis 사용해보기 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트 준비&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;teamsparta.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/DB</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/533</guid>
      <comments>https://annovation.tistory.com/533#entry533comment</comments>
      <pubDate>Tue, 3 Mar 2026 23:36:29 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Spring에서 Redis 사용해보기</title>
      <link>https://annovation.tistory.com/532</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;의존성 추가&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;새 프로젝트 생성 시&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Data Redis (Access+Driver) 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Spring Initializr 로 추가할 때&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKxrZx/dJMcabi1V0t/o839upTnigt5O1p70T1Rz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKxrZx/dJMcabi1V0t/o839upTnigt5O1p70T1Rz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKxrZx/dJMcabi1V0t/o839upTnigt5O1p70T1Rz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKxrZx%2FdJMcabi1V0t%2Fo839upTnigt5O1p70T1Rz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1700&quot; height=&quot;850&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) IntelliJ IDE 로 추가할 때&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-03-02 at 12.12.27 AM.png&quot; data-origin-width=&quot;2278&quot; data-origin-height=&quot;1876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0euiL/dJMcagxPl01/EeX1aFJlUK1JUhx2kTPaz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0euiL/dJMcagxPl01/EeX1aFJlUK1JUhx2kTPaz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0euiL/dJMcagxPl01/EeX1aFJlUK1JUhx2kTPaz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0euiL%2FdJMcagxPl01%2FEeX1aFJlUK1JUhx2kTPaz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2278&quot; height=&quot;1876&quot; data-filename=&quot;Screenshot 2026-03-02 at 12.12.27 AM.png&quot; data-origin-width=&quot;2278&quot; data-origin-height=&quot;1876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;기존 프로젝트에 추가 시&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;build.gradle에 아래 코드 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772378222041&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
  ...
  implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;application.yml 추가&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;src/main/resources 경로에 application.yml 추가&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772379313804&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  data:
    redis:
      host: &amp;lt;서버 주소&amp;gt;
      port: &amp;lt;포트 번호&amp;gt;
      username: &amp;lt;사용자 계정, 기본값 default&amp;gt;
      password: &amp;lt;사용자 비밀번호&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;host와 port를 작성하지 않으면 docker-compose 로 설정해둔 값 혹은 설정한 값이 없다면, 기본값인 localhost:6379에 연결을 시도한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;실습 : Redis에 데이터 저장하기&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;도메인 객체 만들기 : Item.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772546840396&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package, import 생략

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@RedisHash(&quot;item&quot;)
public class Item implements Serializable {
    @Id
    private Long id;
    private String name;
    private String description;
    private Integer price;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772542449731&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;챕터1-4 : Spring에서 Redis 사용해보기 | Notion&quot; data-og-description=&quot;Spring Boot 프로젝트 준비&quot; data-og-host=&quot;teamsparta.notion.site&quot; data-og-source-url=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; data-og-url=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bH3ZKR/dJMb8XkcYXX/8kd85csfqUApyplG1Lk9J0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/XkgBF/dJMb8SpFyZ9/OfTFdk6KHmLbcZqdO37zf1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://teamsparta.notion.site/1-4-Spring-Redis-2242dc3ef5148183b059f5f71a15b28c&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bH3ZKR/dJMb8XkcYXX/8kd85csfqUApyplG1Lk9J0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/XkgBF/dJMb8SpFyZ9/OfTFdk6KHmLbcZqdO37zf1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;챕터1-4 : Spring에서 Redis 사용해보기 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트 준비&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;teamsparta.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/DB</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/532</guid>
      <comments>https://annovation.tistory.com/532#entry532comment</comments>
      <pubDate>Mon, 2 Mar 2026 23:59:42 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Redis 타입 살펴보기 (공부중..)</title>
      <link>https://annovation.tistory.com/530</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Redis 타입 별 명령어&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Redis는 Key - Value 데이터베이스 이다. 그래서 대부분의 명령이 Key를 바탕으로 동작하게 되며, Value로 사용하고자 하는 자료형에 따라 다른 명령어를 사용하게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;또한 대부분의 명령이 존재하지 않는 Key를 이용해도 정상적으로 동작하며, 없는 Key에 데이터를 저장하면 Key를 생성한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;단, 이미 만든 Key에 해당하는 데이터와 다른 자료형의 명령을 사용하는 경우 오류가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;String&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 기본적인 자료형이며, Redis가 Java의 Map&amp;lt;String, String&amp;gt; 처럼 동작한다고 생각할 수 있다.&lt;/li&gt;
&lt;li&gt;단순 문자열이지만, 실제 메모리에는 바이트 배열 (ex. [68 65 6C 6C 6F]) 로 저장된다.&lt;/li&gt;
&lt;li&gt;따라서 바이트로 저장되는 이미지, 음성, 영상, 파일 또는 이메일 본문 등도 보관이 가능하다.&lt;/li&gt;
&lt;li&gt;분산된 구조에서 비교적 큰 사이즈의 데이터를 주고받을 때 Key만 전달해서 데이터 위치를 전달하는 방식으로 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;GET, SET&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SET을 통해 저장할 수 있는 최대 크기는 512MB 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772198172158&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET user:email alex@example.com
GET user:email&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-27 at 10.21.40 PM.png&quot; data-origin-width=&quot;2207&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cacuDR/dJMcaduhErQ/x5eUckIYs4thmvncHmU3Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cacuDR/dJMcaduhErQ/x5eUckIYs4thmvncHmU3Sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cacuDR/dJMcaduhErQ/x5eUckIYs4thmvncHmU3Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcacuDR%2FdJMcaduhErQ%2Fx5eUckIYs4thmvncHmU3Sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2207&quot; height=&quot;926&quot; data-filename=&quot;Screenshot 2026-02-27 at 10.21.40 PM.png&quot; data-origin-width=&quot;2207&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SET &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;lt;key&amp;gt; &amp;lt;value&amp;gt; : key에 value 문자열 데이터를 저장, &quot; 으로 공백 구분&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;GET &amp;lt;key&amp;gt; : key에 저장된 문자열 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;INCR, DECR&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 저장된 데이터가 정수 데이터라면 데이터를 바로 증가, 감소가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772198765402&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET user:count 1
INCR user:count
DECR user:count&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-27 at 10.26.28 PM.png&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;1096&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVwgsy/dJMcaibmRvS/CdT4RdKKC24ZgYXGxaJqWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVwgsy/dJMcaibmRvS/CdT4RdKKC24ZgYXGxaJqWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVwgsy/dJMcaibmRvS/CdT4RdKKC24ZgYXGxaJqWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVwgsy%2FdJMcaibmRvS%2FCdT4RdKKC24ZgYXGxaJqWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2196&quot; height=&quot;1096&quot; data-filename=&quot;Screenshot 2026-02-27 at 10.26.28 PM.png&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;1096&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;INCR key : key에 저장된 데이터를 1 증가&lt;/li&gt;
&lt;li&gt;DECR key : key에 저장된 데이터를 1 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;MSET, MGET&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 Key - Value를 한번에 다루고 싶다면 MSET, MGET 을 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772199181682&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MSET user:name alex user:email alex@example.com
MGET user:name user:email&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MSET key value [key value &amp;hellip;] : key value의 형태로 주어진 인자들을 각 key에 value를 저장&lt;/li&gt;
&lt;li&gt;MGET key [key] : 주어진 모든 key에 해당하는 데이터를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;List&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Linked List 구현체&lt;/li&gt;
&lt;li&gt;Linked List이기 때문에, 중간의 데이터를 활용하는 용도 보다는 양끝의 데이터, 즉 스택 또는 큐 처럼 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;예를 들어, A, B, C 데이터가 Linked List 로 저장되어 있을 때, 양 끝에 데이터를 삽입하고 조회하는 방식으로 활용 가능하다.&lt;/li&gt;
&lt;li&gt;List의 경우 소셜 네트워크에서 많이 사용하는 자료형이다.&lt;br /&gt;ex. List는 데이터가 순서가 있으므로 최신글 -&amp;gt; 오래된글 의 시간 순서로 정렬된 데이터를 저장할 때 유용&lt;/li&gt;
&lt;li&gt;대표적으로 X (구 트위터)가 List를 바탕으로 Timeline을 구성했다고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772605457551&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;X &amp;rarr; A &amp;rarr; B &amp;rarr; C&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis는 Key - Value 형태로 저장되므로 Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; 의 형태로 사용한다고 생각하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;LPUSH, RPUSH, LPOP, RPOP&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PUSH 또는 POP을 L 또는 R과 조합하여, 왼쪽 또는 오른쪽에 데이터를 추가 / 제거가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772605835371&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LPUSH user:list alex  # [alex]
LPUSH user:list brad  # [brad, alex]
RPUSH user:list chad  # [brad, alex, chad]
RPUSH user:list dave  # [brad, alex, chad, dave]

LPOP user:list        # brad
RPOP user:list        # chad&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LPUSH key value : key에 저장된 리스트의 앞쪽에 value를 저장&lt;/li&gt;
&lt;li&gt;RPUSH key value : key에 저장된 리스트의 뒤쪽에 value를 저장&lt;/li&gt;
&lt;li&gt;LPOP key : key에 저장된 리스트의 앞쪽에서 값을 반환 및 제거&lt;/li&gt;
&lt;li&gt;RPOP key : key에 저장된 리스트의 뒤쪽에서 값을 반환 및 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;LLEN, LRANGE&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리스트를 사용하면서 흔히 사용하는 길이 구하기, 범위 내 원소 반환하기 등의 기능도 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772685437211&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LLEN user:list
LRANGE user:list 0 3
LRANGE user:list 0 -1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLEN key : key에 저장된 리스트의 길이를 반환
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;없는 Key를 대상으로 하면 0이 되지만, 다른 자료형을 저장한 Key를 대상으로 하면 &lt;span data-token-index=&quot;1&quot;&gt;오류가 발생&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LRANGE key start end : key의 start부터 end까지 원소들을 반환
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;시작을 0으로 생각하고,&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;end가 실제 길이를 벗어나도 오류가 발생하진 않는다.&lt;/li&gt;
&lt;li&gt;start &amp;gt; end일 경우 빈 결과가 반환된다.&lt;/li&gt;
&lt;li&gt;음수의 경우 리스트의 뒤에서부터 데이터를 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Set&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Set은 &lt;/span&gt;순서가 없고 중복되지 않는 문자열들의 집합이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;SADD, SREM, SMEMBERS, SISMEMBER, SCARD&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772697915001&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SADD user:java alex  # [alex]
SADD user:java brad  # [alex, brad]
SADD user:java chad  # [alex, brad, chad]
SREM user:java alex  # [brad, chad]

SMEMBERS user:java        # [alex, brad, chad]
SISMEMBER user:java brad  # true
SISMEMBER user:java dave  # false&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SADD key value : key에 저장된 집합에 value를 추가&lt;/li&gt;
&lt;li&gt;SREM key value : key에 저장된 집합의 value를 제거&lt;/li&gt;
&lt;li&gt;SMEMBERS key : key에 저장된 집합의 모든 원소를 반환&lt;/li&gt;
&lt;li&gt;SISMEMBER key value : key에 저장된 집합에 value가 존재하는지 반환&lt;/li&gt;
&lt;li&gt;SCARD key : key에 저장된 집합의 크기를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;SINTER, SUNION, SINTERCARD&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기에 복수의 집합이 있다면, 교집합, 합집합 등의 기능을 제공한다.&lt;/li&gt;
&lt;li&gt;결과 집합 자체를 반환하기도, 결과 집합의 크기를 반환하기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772697950926&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 다른 Set에 추가한 뒤,
SADD user:python alex
SADD user:python dave

SINTER user:java user:python        # [alex]
SUNION user:java user:python        # [alex, brad, chad, dave]
SINTERCARD 2 user:java user:python  # 1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SINTER key1 key2 : key1과 key2에 저장된 집합들의 교집합의 원소들을 반환&lt;/li&gt;
&lt;li&gt;SUNION key1 key2 : key1과 key2에 저장된 집합들의 합집합의 원소들을 반환&lt;/li&gt;
&lt;li&gt;SINTERCARD number key1 [key2 ...] : number개의 key에 저장된 집합들의 교집합의 크기를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;SISMEMBER&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772698115936&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SADD user:java alex   # [alex]
SADD user:java brad   # [alex, brad]
SADD user:java chad   # [alex, brad, chad]

SISMEMBER user:java brad   # true
SISMEMBER user:java dave   # false&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복을 허용하지 않으며, 어떤 데이터의 존재 여부를 확인하는 SISMEMBER 같은 경우 O(1)의 시간복잡도를 가지고 있다.&lt;/li&gt;
&lt;li&gt;그래서 중복 없는 방문 수, 인증 토큰 블랙리스트 등을 구현할때 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️단, 매우 높은 방문수를 기록하는 서비스의 경우 실제 데이터가 필요하지 않다면, 근사값을 돌려주는 확률형 자료형을 사용하는게 권장됩니다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Hash&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Field - Value 쌍으로 이뤄진 자료형&lt;/li&gt;
&lt;li&gt;Hash 데이터를 가져오기 위해 Key를 사용하고, 이후 다시 Key에 저장된 Hash 데이터에 Field - Value 쌍을 넣어주는 형식으로 동작&lt;/li&gt;
&lt;li&gt;즉, Redis 전체가 Map이라면 Hash는 Map&amp;lt;String, Map&amp;lt;String, String&amp;gt;&amp;gt;의 형식이라고 생각할 수 있다.&lt;/li&gt;
&lt;li&gt;Hash는 본래 하나의 키에 복잡한 데이터 (객체의 데이터 라던지)를 하나의 키에 저장하는 용도로 주로 활용되고, 공식 문서에서도 여러 Key에 걸쳐 객체의 데이터를 표현하기 보단 Hash를 자주 활용할 것을 권장하고 있다. (&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/hashes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;장바구니 같은 기능은 사용자별로, 어떤 물품이 몇개나 담겨있는지와 같은 정보가 포함되어야 한다. 사용자 마다 Hash 데이터를 생성하고, &lt;span data-token-index=&quot;1&quot;&gt;물품 - 갯수&lt;/span&gt; 형식으로 데이터를 저장하면 사용자별 장바구니를 쉽게 저장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;While hashes are handy to represent objects ...&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;HSET, HGET, HMGET, HGETALL, HKEYS, HLEN&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772698510897&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HSET user:alex name alex age 25
HSET user:alex major CSE

HGET user:alex name
HGET user:alex age

HMGET user:alex age major
HGETALL user:alex

HKEYS user:alex
HLEN user:alex&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HSET key field value [field value] : key의 Hash에 field에 value를 넣는다. 한번에 여러 field - value 쌍을 넣어줄 수 있다.&lt;/li&gt;
&lt;li&gt;HGET key field:&amp;nbsp; key에 저장된 Hash의 field에 저장된 value를 반환. 없는 field의 경우 null&lt;/li&gt;
&lt;li&gt;HMGET key field [field] : key에 저장된 Hash에서 복수의 field에 저장된 value를 반환&lt;/li&gt;
&lt;li&gt;HGETALL key : key에 저장된 Hash에 저장된 field - value를 전부 반환&lt;/li&gt;
&lt;li&gt;HKEYS key : key에 저장된 Hash에 저장된 field를 전부 반환&lt;/li&gt;
&lt;li&gt;HLEN key : key에 저장된 Hash에 저장된 field의 갯수를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Sorted Set&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름처럼 정렬된 집합이다.&lt;/li&gt;
&lt;li&gt;Set과 동일하게, 유일한 값들만 유지하지만 여기에 더해 각 값들에 score라고하는 실수를 함께 보관한다.&lt;/li&gt;
&lt;li&gt;그리고 데이터를 가져올 때, &lt;span data-token-index=&quot;1&quot;&gt;score를 바탕으로 정렬&lt;/span&gt;하여 값들을 가져올 수 있다.&lt;/li&gt;
&lt;li&gt;대표적으로 리더보드나 Rate Limiter, 즉 순위와 관련된 기능을 만드는데 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✅ Rate Limiter란, API 등을 제공할 때 짧은 시간에 지나치게 많은 요청을 막기 위한 기능을 의미합니다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;ZADD, ZINCRBY, ZRANK, ZRANGE, ZREVRANK, ZREVRANGE&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772698991790&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ZADD user:ranks 10 alex
ZADD user:ranks 9 brad 11 chad
ZADD user:ranks 8 dave

ZINCRBY user:ranks 2 alex

ZRANK user:ranks alex
ZRANGE user:ranks 0 3

ZREVRANK user:ranks alex
ZREVRANGE user:ranks 0 3&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ZADD key score member [score member ...] : key의 Sorted Set에 score를 점수로 가진 member를 추가, 이미 있는 member의 경우 새로운 score를 설정&lt;/li&gt;
&lt;li&gt;ZRANK key member : key의 Sorted Set의 member의 순위를 오름차순 기준으로 0에서 부터 세서 반환&lt;/li&gt;
&lt;li&gt;ZRANGE key start stop : key의 Sorted Set의 member들을 start 부터 stop 순위까지 오름차순 기준으로 반환&lt;/li&gt;
&lt;li&gt;ZREVRANK key member : key의 Sorted Set의 member의 순위를 내림차순 기준으로 0에서 부터 세서 반환&lt;/li&gt;
&lt;li&gt;ZREVRANGE key start stop : key의 Sorted Set의 member들을 start 부터 stop 순위까지 내림차순 기준으로 반환&lt;/li&gt;
&lt;li&gt;ZINCRBY key increment member : key의 Sorted Set의 member의 score를 increment 만큼 증가 (음수를 전달하면 감소)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;그 외 공용 명령&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 외 자료형과 상관없이 사용할 수 있는 명령들이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;DEL, EXPIRE, EXPIRETIME&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대표적으로 많이 사용하는 건, Key를 제거하기 위한 DEL, 만료시각 설정을 위한 EXPIRE 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772699122427&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET somekey &quot;to be deleted&quot;
DEL somekey

SET expirekey &quot;to be expired&quot;
EXPIRE expirekey 5
EXPIRETIME expirekey&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DEL key : key(와 저장된 데이터)를 제거&lt;/li&gt;
&lt;li&gt;EXPIRE key seconds : key의 TTL(유효시각)을 seconds로 설정, seconds초가 지나면 key 제거&lt;/li&gt;
&lt;li&gt;EXPIRETIME key : key가 만료되는 시각을 Unix Timestamp로 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot; data-token-index=&quot;0&quot;&gt;KEYS *&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 Key를 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772699171699&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;KEYS *&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;glob 패턴 형식을 전달하여, 패턴에 일치하는 키를 반환하는 메서드이다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #eb5757; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;*&lt;/span&gt; &lt;/span&gt;은 임의갯수의 글자를 대체하는 와일드카드입니다. 지금은 * 만 있으므로, 모든 Key와 일치하는 패턴이기 때문에 모든 Key가 반환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;✅ glob 패턴 이란?&lt;br /&gt;Linux 파일시스템 등에서 흔히 사용하며, 와일드카드(wildcard)를 이용해 문자열을 매칭하는 간단한 패턴 규칙이다.&lt;br /&gt;&lt;br /&gt;ex. &lt;br /&gt;* 모든 문자열&lt;br /&gt;? 한 글자 &lt;br /&gt;[abc] 특정 문자 &lt;br /&gt;[a-z] 문자 범위&lt;br /&gt;&lt;br /&gt;ex.&lt;br /&gt;h?llo - hello, hallo, hxllo&lt;br /&gt;h*llo - hllo, heeeello &lt;br /&gt;h[ae]llo - hello, hallo (not hillo) &lt;br /&gt;h[^e]llo - hallo, hbllo, ... (not hello) &lt;br /&gt;h[a-b]llo - hallo, hbllo&lt;br /&gt;&lt;br /&gt;✅ 와일드카드(wildcard) 란?&lt;br /&gt;임의의 문자나 문자열을 대신해서 매칭되는 특수 문자&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot; data-token-index=&quot;0&quot;&gt;FLUSHDB&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 Key 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772699515038&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FLUSHDB&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-3-Redis-2242dc3ef5148154a4f6c97065cf1b51&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://teamsparta.notion.site/1-3-Redis-2242dc3ef5148154a4f6c97065cf1b51&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772195275628&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;챕터1-3 : Redis 타입 살펴보기 | Notion&quot; data-og-description=&quot;Redis를 설치하고 성공적으로 연결했다면, 이제 데이터를 저장해 봅시다.&quot; data-og-host=&quot;teamsparta.notion.site&quot; data-og-source-url=&quot;https://teamsparta.notion.site/1-3-Redis-2242dc3ef5148154a4f6c97065cf1b51&quot; data-og-url=&quot;https://teamsparta.notion.site/1-3-Redis-2242dc3ef5148154a4f6c97065cf1b51&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RLzQU/dJMb84XV21Q/mQl1EEvMGvlz5fAVWHwXB0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/AEnP5/dJMb85vMdF9/vTEzFJXu5bCMmLZDjkaKGK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-3-Redis-2242dc3ef5148154a4f6c97065cf1b51&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://teamsparta.notion.site/1-3-Redis-2242dc3ef5148154a4f6c97065cf1b51&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RLzQU/dJMb84XV21Q/mQl1EEvMGvlz5fAVWHwXB0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/AEnP5/dJMb85vMdF9/vTEzFJXu5bCMmLZDjkaKGK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;챕터1-3 : Redis 타입 살펴보기 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Redis를 설치하고 성공적으로 연결했다면, 이제 데이터를 저장해 봅시다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;teamsparta.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/DB</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/530</guid>
      <comments>https://annovation.tistory.com/530#entry530comment</comments>
      <pubDate>Fri, 27 Feb 2026 22:42:54 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Redis 설치하기 (MacOS)</title>
      <link>https://annovation.tistory.com/529</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;로컬 환경에 설치하기&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Homebrew를 이용해 Redis 설치&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Homebrew를 설치한 적 없다면, Homebrew 먼저 설치&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Homebrew 설치 페이지 : &lt;a href=&quot;https://brew.sh/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://brew.sh/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1772109644469&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Homebrew&quot; data-og-description=&quot;The Missing Package Manager for macOS (or Linux).&quot; data-og-host=&quot;brew.sh&quot; data-og-source-url=&quot;https://brew.sh/&quot; data-og-url=&quot;https://brew.sh/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5FSQh/dJMb85WQuu9/XXMqa0KAEX4yBtwieRc9VK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/ceCysS/dJMb88654KY/KRNKUuinI3iuWo3ZAFZ28k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://brew.sh/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://brew.sh/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5FSQh/dJMb85WQuu9/XXMqa0KAEX4yBtwieRc9VK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/ceCysS/dJMb88654KY/KRNKUuinI3iuWo3ZAFZ28k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Homebrew&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Missing Package Manager for macOS (or Linux).&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;brew.sh&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Homebrew 설치 명령어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772109661315&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Homebrew를 이용해 Redis를 설치&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 명령어를 터미널에 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772109690511&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install redis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 백그라운드에서 Redis 실행&lt;/p&gt;
&lt;pre id=&quot;code_1772109727920&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services start redis&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Docker에 설치하기 (권장)&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Docker 사용을 권장하는 이유&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너화된 환경을 제공함으로써 OS 차이, 라이브러리 버전 차이 등으로 인한 오류를 예방할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;docker-compose.yml 이 필요한 이유&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 컨테이너로 구성된 애플리케이션을 YAML 파일로 정의하고 한 번의 명령어로 실행할 수 있게 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Docker 이용해 Redis 설치하기&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker가 설치되어 있다면 명령어 하나로 Redis를 실행해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;907&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvcTKn/dJMcah4zUmn/fmbHXxX2Gi2ZfK5ym6KIr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvcTKn/dJMcah4zUmn/fmbHXxX2Gi2ZfK5ym6KIr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvcTKn/dJMcah4zUmn/fmbHXxX2Gi2ZfK5ym6KIr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvcTKn%2FdJMcah4zUmn%2FfmbHXxX2Gi2ZfK5ym6KIr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;814&quot; data-origin-width=&quot;907&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;redis&lt;/b&gt;&lt;/span&gt; : Redis의 기본(Core) 서버 이미지로, 가장 표준적인 Redis 기능만 포함된 최소 구성 버전입니다. 단순 캐시&amp;middot;세션&amp;middot;Pub/Sub 등 기본 기능 실습에 적합&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;redis/redis-stack-server&lt;/b&gt;&lt;/span&gt; : 기본 Redis에 Redis Stack 모듈(ex. JSON, Search, Bloom, TimeSeries 등)이 포함된 서버 이미지이다. JSON 문서 저장, 전문 검색, 확률형 자료구조 등을 사용할 때 선택한다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;redis/redis-stack&lt;/b&gt;&lt;/span&gt; : &lt;span&gt;redis/redis-stack-server&lt;/span&gt;에 Redis Insight(웹 기반 관리 GUI)가 추가된 통합 이미지이다. 서버 기능과 함께 시각적 관리 도구까지 함께 사용하려는 경우 적합하다.&lt;/li&gt;
&lt;li&gt;Docker Hub에서 Redis를 찾아보면 세가지 이미지가 상단에 나오게 되는데, 이중 한가지를 선택해서 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️Docker Hub 란?&lt;br /&gt;&lt;br /&gt;컨테이너 이미지를 저장하고(push) 검색하고 다운로드(pull) 할 수 있는 공식 이미지 레지스트리(registry) 서비스이다.&lt;br /&gt;&lt;br /&gt;컨테이너(Container) : 이미지를 실행한 인스턴스로, 격리된 환경에서 애플리케이션이 동작하는 실행 단위&lt;br /&gt;이미지(Image) : 컨테이너를 생성하기 위한 실행 파일과 라이브러리, 설정을 포함한 불변 템플릿&lt;br /&gt;레지스트리(Registry) : Docker 이미지를 저장&amp;middot;관리하고 네트워크를 통해 배포하는 저장소 시스템&lt;br /&gt;도커 허브(Docker Hub) : 컨테이너 이미지를 저장하고 공유하는 Docker 공식 public registry&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Run Redis Stack on Docker (&lt;a href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/install/archive/install-stack/docker/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 공식 문서에 Docker 를 통해 Redis Stack 이미지를 다운 받는 방법이 안내되어있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 프로젝트 루트에 docker-compose.yml 을 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772114473642&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  redis-stack:
    image: redis/redis-stack
    container_name: redis-stack-compose
    restart: always
    environment:
      REDIS_ARGS: &quot;--requirepass systempass&quot;
    ports:
      - 6379:6379
      - 8001:8001&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 예시의 systempass가 비밀번호의 역할을 한다. 이 부분을 수정하면 접속할 때 비밀번호가 바뀌니 주의!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. docker-compose.yml 이 있는 폴더에서 터미널은 열어 아래 명령어를 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772115157908&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동으로 최신 버전의 Redis Stack 이미지를 사용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Redis 컨테이너가 정상적으로 실행되었는지 확인한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772115234291&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Up 3 minutes : 컨테이너 프로세스가 살아 있음을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QhlS3/dJMcaih6kIN/1ItJJliDf29TuAPsoXKCZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QhlS3/dJMcaih6kIN/1ItJJliDf29TuAPsoXKCZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QhlS3/dJMcaih6kIN/1ItJJliDf29TuAPsoXKCZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQhlS3%2FdJMcaih6kIN%2F1ItJJliDf29TuAPsoXKCZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1072&quot; height=&quot;181&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Intellij IDEA UE에서 연결해보기&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. &lt;span style=&quot;color: #333333;&quot;&gt;Database 탭&lt;/span&gt;을 찾아 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;603&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tgsDq/dJMcaca745Q/hzSuKGiwZqVmqTKkkGKrOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tgsDq/dJMcaca745Q/hzSuKGiwZqVmqTKkkGKrOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tgsDq/dJMcaca745Q/hzSuKGiwZqVmqTKkkGKrOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtgsDq%2FdJMcaca745Q%2FhzSuKGiwZqVmqTKkkGKrOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;603&quot; height=&quot;327&quot; data-origin-width=&quot;603&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2&lt;span style=&quot;color: #333333;&quot;&gt;. + 를 누른 다음, Data Source &amp;gt; Redis를 찾아거나, 검색을 통해 Data Source를 선택한 다음, Redis를 입력하면 쉽게 찾을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3HUI3/dJMcacCcCeP/k0hdyMRKnCTkuiZlAkOHnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3HUI3/dJMcacCcCeP/k0hdyMRKnCTkuiZlAkOHnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3HUI3/dJMcacCcCeP/k0hdyMRKnCTkuiZlAkOHnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3HUI3%2FdJMcacCcCeP%2Fk0hdyMRKnCTkuiZlAkOHnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;908&quot; height=&quot;505&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;span style=&quot;color: #333333;&quot;&gt;자신의 Redis 인스턴스의 Host와 Port(위의 과정을 따랐다면 localhost, 6379)를, 이후 User는 default, Password는 로컬에 설치한 경우 생략, Docker로 설치한 경우 자신이 password에 넣은 값을 넣어준다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q0l3d/dJMcadA153R/vGZPcfmZJIGfloNIHDx7XK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q0l3d/dJMcadA153R/vGZPcfmZJIGfloNIHDx7XK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q0l3d/dJMcadA153R/vGZPcfmZJIGfloNIHDx7XK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq0l3d%2FdJMcadA153R%2FvGZPcfmZJIGfloNIHDx7XK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;471&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 이후&lt;span style=&quot;color: #333333;&quot;&gt; Test Connection을 클릭해서 잘 연결되면, OK를 누른다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm1Rvi/dJMcafZX83N/yl3G5AbVkKwIUBzIwEOuuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm1Rvi/dJMcafZX83N/yl3G5AbVkKwIUBzIwEOuuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm1Rvi/dJMcafZX83N/yl3G5AbVkKwIUBzIwEOuuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm1Rvi%2FdJMcafZX83N%2Fyl3G5AbVkKwIUBzIwEOuuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;326&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. console 탭에서 명령어들을 사용해서 데이터를 가져올 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령어 예시 (Command + Enter 로 명령어 한 줄 씩 실행 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772116007128&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET start &quot;hello world&quot;
GET start&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;console 탭 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J8i1w/dJMcagYS2jE/q7pEhD74zFqpNh6NUM2ej0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J8i1w/dJMcagYS2jE/q7pEhD74zFqpNh6NUM2ej0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J8i1w/dJMcagYS2jE/q7pEhD74zFqpNh6NUM2ej0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ8i1w%2FdJMcagYS2jE%2Fq7pEhD74zFqpNh6NUM2ej0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;460&quot; height=&quot;227&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;227&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령어를 통해 가져온 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nfEAS/dJMcab4lEWU/GKg8wKn3u07XHGyHlR2XeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nfEAS/dJMcab4lEWU/GKg8wKn3u07XHGyHlR2XeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nfEAS/dJMcab4lEWU/GKg8wKn3u07XHGyHlR2XeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnfEAS%2FdJMcab4lEWU%2FGKg8wKn3u07XHGyHlR2XeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;258&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️console 탭이 사라지면 Database 탭에서 다시 열 수 있다&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T6sRw/dJMcafMtOro/vh1JP0BlV204XFbMIKINs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T6sRw/dJMcafMtOro/vh1JP0BlV204XFbMIKINs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T6sRw/dJMcafMtOro/vh1JP0BlV204XFbMIKINs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT6sRw%2FdJMcafMtOro%2Fvh1JP0BlV204XFbMIKINs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;475&quot; height=&quot;300&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Redis Insight를 이용해 연결해보기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Redis Insight 란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 전용 IDE (ex. MySQL Workbench)&lt;/li&gt;
&lt;li&gt;간단한 기능 테스트만 할 때는 엄청 필요한 도구는 아닐 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;App Store 에서 설치하기&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VMH7d/dJMcaaEoJKD/fP0J3uTkKavMQMf7shF8nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VMH7d/dJMcaaEoJKD/fP0J3uTkKavMQMf7shF8nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VMH7d/dJMcaaEoJKD/fP0J3uTkKavMQMf7shF8nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVMH7d%2FdJMcaaEoJKD%2FfP0J3uTkKavMQMf7shF8nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;649&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MacOS의 경우 App Store 에 등록된 Redis Insight 를 다운로드 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Docker 로 설치하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 위에 소개된 docker-compose.yml 를 사용한 경우, Redis Insight 도 같이 설치된다.&lt;/p&gt;
&lt;pre id=&quot;code_1772116572048&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  redis-stack:
    image: redis/redis-stack
    container_name: redis-stack-compose
    restart: always
    environment:
      REDIS_ARGS: &quot;--requirepass systempass&quot;
    ports:
      - 6379:6379
      - 8001:8001&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;redis/redis-stack : 이 이미지는 Redis 서버 + Redis Stack 모듈 + Redis Insight까지 포함된 통합 이미지이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 따라서 바로 localhost:8001 로 들어가면, 웹 애플리케이션으로 확인 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-26 at 11.41.27 PM.png&quot; data-origin-width=&quot;2195&quot; data-origin-height=&quot;1231&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TX6pv/dJMcagkhtHD/fRdbb3TUwUbpkGvFSixEqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TX6pv/dJMcagkhtHD/fRdbb3TUwUbpkGvFSixEqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TX6pv/dJMcagkhtHD/fRdbb3TUwUbpkGvFSixEqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTX6pv%2FdJMcagkhtHD%2FfRdbb3TUwUbpkGvFSixEqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2195&quot; height=&quot;1231&quot; data-filename=&quot;Screenshot 2026-02-26 at 11.41.27 PM.png&quot; data-origin-width=&quot;2195&quot; data-origin-height=&quot;1231&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Username 과 Password를 입력 후 Test Connection 을 진행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-26 at 11.45.42 PM.png&quot; data-origin-width=&quot;2189&quot; data-origin-height=&quot;1231&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csaKh6/dJMcagYS2x3/3CuDzexGgSL6ZsNiqqbZh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csaKh6/dJMcagYS2x3/3CuDzexGgSL6ZsNiqqbZh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csaKh6/dJMcagYS2x3/3CuDzexGgSL6ZsNiqqbZh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsaKh6%2FdJMcagYS2x3%2F3CuDzexGgSL6ZsNiqqbZh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2189&quot; height=&quot;1231&quot; data-filename=&quot;Screenshot 2026-02-26 at 11.45.42 PM.png&quot; data-origin-width=&quot;2189&quot; data-origin-height=&quot;1231&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection이 성공했다면, Apply Changes 클릭하면 왼쪽 메뉴 바에 새로운 기능들이 활성화 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-2-Redis-2242dc3ef51481d0b7a7d2f163d2df05&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://teamsparta.notion.site/1-2-Redis-2242dc3ef51481d0b7a7d2f163d2df05&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772109077739&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;챕터1-2 : Redis 설치하기 | Notion&quot; data-og-description=&quot;코드스니펫&quot; data-og-host=&quot;teamsparta.notion.site&quot; data-og-source-url=&quot;https://teamsparta.notion.site/1-2-Redis-2242dc3ef51481d0b7a7d2f163d2df05&quot; data-og-url=&quot;https://teamsparta.notion.site/1-2-Redis-2242dc3ef51481d0b7a7d2f163d2df05&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cFfok7/dJMb8QejpNE/KOMkCBJqYgbAetJ6Fgftb0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/zVEDV/dJMb9jgupj5/eF6b1Mvn2F1wAh4OQyMe8k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-2-Redis-2242dc3ef51481d0b7a7d2f163d2df05&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://teamsparta.notion.site/1-2-Redis-2242dc3ef51481d0b7a7d2f163d2df05&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cFfok7/dJMb8QejpNE/KOMkCBJqYgbAetJ6Fgftb0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/zVEDV/dJMb9jgupj5/eF6b1Mvn2F1wAh4OQyMe8k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;챕터1-2 : Redis 설치하기 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코드스니펫&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;teamsparta.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/DB</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/529</guid>
      <comments>https://annovation.tistory.com/529#entry529comment</comments>
      <pubDate>Thu, 26 Feb 2026 22:41:43 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 인메모리 저장소와 Redis 기초</title>
      <link>https://annovation.tistory.com/528</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;인메모리 저장소&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;In-Memory = 메모리 안에서 동작한다는 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;인메모리 저장소의 필요성&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 기반의 애플리케이션에서 일반적으로 사용하는 데이터 저장소는 관계형 데이터베이스(RDBMS)이다.&lt;/li&gt;
&lt;li&gt;관계형 데이터베이스의 주된 목적은 &lt;span&gt;&lt;b&gt;영속성(Persistence)&lt;/b&gt;&lt;/span&gt; 보장이다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;영속성 데이터란, &lt;/span&gt;&lt;b&gt;애플리케이션이 종료되더라도 유지되어야 하는 데이터&lt;/b&gt;&lt;span&gt;를 의미한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;예를 들어, 회원 정보, 주문 내역, 결제 기록, 게시글과 같은 데이터들은 파일시스템에 데이터를 저장함으로서 서비스가 종료되어도 유지되어야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️파일시스템(File System)이란?&lt;br /&gt;&lt;br /&gt;파일시스템은 운영체제가 디스크(SSD, HDD)에 데이터를 저장하고 관리하는 구조이다.&lt;br /&gt;데이터를 파일 단위로 저장한다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 서비스를 만들다보면 영구 보관이 필요없는 일시적인 데이터를 저장해야 하는 상황이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;예를 들어, 로그인 정보, 장바구니, 조회수 카운팅, 실시간 랭킹과 같은 기능은 사용자의 행동에 따라 비번하게 데이터의 수정이 발생한다.&lt;/li&gt;
&lt;li&gt;이런 상황에서 파일시스템에 데이터를 저장하는 것은 특성상 속도가 느려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️파일시스템(File System) 기반 저장이 상대적으로 느린 이유?&lt;br /&gt;&lt;br /&gt;디스크 기반 저장이기 때문이다. 디스크에 데이터를 쓰는 행위는 물리적인 I/O 작업이고 메모리 접근에 비해 속도가 매우 느리다.&lt;br /&gt;이에 반해 데이터를 디스크가 아닌 RAM에 저장하는 인메모리 저장 방식은 디스크에 저장하는 파일시스템 방식에 비해 속도가 매우 빠르다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;대표적인 인메모리 DB : Redis&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Redis 란?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis는 REmote DIctionary Server를 줄인말로, Java의 Map과 같은 방식으로 데이터를 저장하는 데이터베이스이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772034774894&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;user:1&quot;
    ├── name &amp;rarr; sati
    └── age  &amp;rarr; 30&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Redis 특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 관계형 데이터베이스와 다른 가장 큰 특징은 In-Memory 데이터베이스이다.&lt;/li&gt;
&lt;li&gt;메모리, 즉 RAM에 데이터를 저장하기 때문에 복잡한 입출력 과정 필요 없다.&lt;/li&gt;
&lt;li&gt;RDBMS(관계형 데이터베이스)에 비해 더 빠르게 동작하는 대신, 언제든 사라질 수 있는 데이터를 다룬다는 차이를 가진다.&lt;/li&gt;
&lt;li&gt;특정 게시글의 조회수와 같이 빠르게 업데이트되는 데이터, 사용자 세션, 장바구니와 같은 시간이 지나면 삭제되는 데이터 등을 저장하기 위해 가장 많이 사용된다.&lt;/li&gt;
&lt;li&gt;대표적인 NoSQL 데이터베이스로, 데이터를 단순히 Key, Value를 저장하는 것이 아닌 자료 구조 (data structure)를 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;NoSQL&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;NoSQL 이란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NoSQL은 데이터베이스를 만드는 접근법의 일종으로, 'Not Only SQL'을 의미한다.&lt;/li&gt;
&lt;li&gt;RDBMS(관계형 데이터베이스)는 일반적으로 테이블 형식으로 데이터를 저장하고, 그 데이터를 회수하기 위해 SQL을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772035765433&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM users;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반면 Redis는 단순 문자열(String)부터, 리스트, 집합, Hash 등 다양한 형태의 데이터를 저장하며, 이 데이터를 회수하기 위해 SQL을 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772035804913&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET greeting &quot;Hello, Redis!&quot;
GET greeting&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 관계형 데이터베이스가 약점을 가진 확장성, 유연성, 성능에 대한 문제를 해결하기 위해 사용되는 경우가 많으며, 컴퓨터 기술의 발전과 웹의 활성화로 인해 비정형 데이터를 더 높은 성능으로 사용하는데 초점이 맞춰진 경우가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;NoSQL의 데이터 관리 방식&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/exGDAp/dJMcahQ1AX5/dAIwbnWMEvKUuQ8NGxNzY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/exGDAp/dJMcahQ1AX5/dAIwbnWMEvKUuQ8NGxNzY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exGDAp/dJMcahQ1AX5/dAIwbnWMEvKUuQ8NGxNzY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FexGDAp%2FdJMcahQ1AX5%2FdAIwbnWMEvKUuQ8NGxNzY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;179&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미리 정의된 구조(schema : ex. 데이터 타입, NULL 허용 여부, 기본값, 제약 조건, 인덱스, 외래키 등)와 SQL을 사용하는 RDBMS와 달리, NoSQL은 데이터를 관리하는 방법이 다양하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Key-Value (ex. Redis)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 단순한 형태의 데이터베이스로, Key에 Value를 저장하는 형태이다.&lt;/li&gt;
&lt;li&gt;JSON, Python의 Dictionary, Java의 Map의 형태로 데이터를 관리한다고 생각할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772037073012&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;user:1&quot; &amp;rarr; &quot;{ name: 'sati', age: 30 }&quot;
&quot;order:1&quot; &amp;rarr; &quot;{ userId: 1, items: ['apple', 'banana'] }&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Document (ex. MongoDB)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 표현하는 Document라는 단위로 데이터를 저장하는 형태이다.&lt;/li&gt;
&lt;li&gt;Key - Value에서 발전했다고 볼 수 있으며, JSON, XML 등 복잡한 데이터를 저장하고 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772037087328&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;userId&quot;: 1,
  &quot;name&quot;: &quot;sati&quot;,
  &quot;orders&quot;: [
    {
      &quot;orderId&quot;: 1,
      &quot;items&quot;: [
        { &quot;product&quot;: &quot;apple&quot;, &quot;qty&quot;: 2 },
        { &quot;product&quot;: &quot;banana&quot;, &quot;qty&quot;: 1 }
      ]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Column-Family (ex. cassandra)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Row의 Column이 고정되어있지 않고, 필요한 데이터 Column을 이름, 데이터, Timestamp와 함께 저장하는 형태의 데이터베이스 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772037100303&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PRIMARY KEY (user_id, order_id)

user_id | order_id | product | qty
-----------------------------------
1       | 1        | apple   | 2
1       | 1        | banana  | 1&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Redis 활용 사례&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis는 NoSQL 중에서도 Key-Value Store로 작동하는 인메모리 데이터베이스이며, 지연이 적은 읽기 / 쓰기 성능을 가졌다.&lt;/li&gt;
&lt;li&gt;그렇기 때문에 일시적인 데이터, 변경이 잦은 데이터를 다뤄야 되는 상황에서 많이 활용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Session Clustering : 여러 애플리케이션 인스턴스에서 같은 세션 정보를 사용할 수 있도록 도와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Caching : 자주 사용되는 데이터를 저장해두어, 데이터베이스 조회를 줄이고 전반적인 응답속도를 개선한다. (웹 어플리케이션의 성능에 많은 영향을 미친다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 지원하는 다양한 자료구조를 바탕으로 리더보드, 방문수 트래킹, 좌표 기반 검색 등의 기능을 쉽게 구현할 수 있게 해준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-1-Redis-2242dc3ef514811a85e0e562dfcae13c&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://teamsparta.notion.site/1-1-Redis-2242dc3ef514811a85e0e562dfcae13c&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772030457087&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;챕터1-1 : 인메모리 저장소와 Redis 기초 | Notion&quot; data-og-description=&quot;인메모리 저장소의 필요성&quot; data-og-host=&quot;teamsparta.notion.site&quot; data-og-source-url=&quot;https://teamsparta.notion.site/1-1-Redis-2242dc3ef514811a85e0e562dfcae13c&quot; data-og-url=&quot;https://teamsparta.notion.site/1-1-Redis-2242dc3ef514811a85e0e562dfcae13c&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Ja5I4/dJMb9b3Pbdf/FoSHKuJJQTQqW1jQo3KTOk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/wYmMH/dJMb9hCYqAp/1j1k0yXF80h09GM3w6uftk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://teamsparta.notion.site/1-1-Redis-2242dc3ef514811a85e0e562dfcae13c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://teamsparta.notion.site/1-1-Redis-2242dc3ef514811a85e0e562dfcae13c&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Ja5I4/dJMb9b3Pbdf/FoSHKuJJQTQqW1jQo3KTOk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/wYmMH/dJMb9hCYqAp/1j1k0yXF80h09GM3w6uftk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;챕터1-1 : 인메모리 저장소와 Redis 기초 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인메모리 저장소의 필요성&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;teamsparta.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/DB</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/528</guid>
      <comments>https://annovation.tistory.com/528#entry528comment</comments>
      <pubDate>Wed, 25 Feb 2026 23:58:05 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 재고 감소 통합 테스트 코드 6 - Optimistic Lock(낙관적 락) 멀티 스레드 테스트 코드</title>
      <link>https://annovation.tistory.com/527</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Optimistic Lock(낙관적 락) 테스트 코드&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Optimistic Lock(낙관적 락) 을 적용한 동시성 처리 테스트 코드를 분석해보자 !&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;테스트 코드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771812555642&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;재고 감소 - 100개 동시 요청 시 정확히 차감&quot;)
void decreaseStock_when100ConcurrentRequests_thenSuccess() throws Exception {

    // given
    int threadCount = 100;

    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

    CountDownLatch countDownLatch = new CountDownLatch(threadCount);

    List&amp;lt;Future&amp;lt;?&amp;gt;&amp;gt; futures = new ArrayList&amp;lt;&amp;gt;();

	// when
    for (int i = 0; i &amp;lt; threadCount; i++) {
        futures.add(executorService.submit(() -&amp;gt; {
            try {
                stockRetryFacade.decreaseWithRetry(StockFixture.decreaseRequest(product, 1));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            } finally {
                countDownLatch.countDown();
            }
        }));
    }

    countDownLatch.await();

    for (Future&amp;lt;?&amp;gt; future : futures) {
        future.get(); // 스레드 내부 예외를 테스트 실패로 반영
    }

    executorService.shutdown();

    // then
    Stock updatedStock = jpaStockRepository
            .findByProductId(product.getProductId())
            .orElseThrow();

    assertEquals(0, updatedStock.getQuantity());
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;given&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;given : 테스트 환경 준비&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 동시에 실행할 스레드 개수 설정&lt;/p&gt;
&lt;pre id=&quot;code_1771934493587&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int threadCount = 100;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 스레드 풀 생성 (100개의 스레드를 동시에 실행할 수 있는 환경)&lt;/p&gt;
&lt;pre id=&quot;code_1771934518763&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedThreadPool(threadCount);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;newFixedThreadPool(100): 최대 100개의 작업을 동시에 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) CountDownLatch : 모든 스레드가 작업을 완료할 때까지 대기하기 위한 동기화 도구&lt;/p&gt;
&lt;pre id=&quot;code_1771934580558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CountDownLatch countDownLatch = new CountDownLatch(threadCount);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;100으로 초기화 &amp;rarr; 각 스레드가 countDown()을 호출할 때마다 1씩 감소&lt;/li&gt;
&lt;li&gt;0이 되면 await()에서 대기 중인 메인 스레드가 계속 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) Future 리스트 : 각 스레드의 실행 결과를 추적하기 위한 컨테이너&lt;/p&gt;
&lt;pre id=&quot;code_1771934598657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Future&amp;lt;?&amp;gt;&amp;gt; futures = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Future를 통해 스레드 내부에서 발생한 예외를 감지할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;when&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;when : 실제 테스트 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771934763824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 100번 반복하면서 100개의 재고 감소 작업을 동시에 제출
for (int i = 0; i &amp;lt; threadCount; i++) {

    // executorService.submit(): 작업을 스레드 풀에 제출
    // submit()은 Future 객체를 반환 &amp;rarr; 나중에 결과 확인 가능
    futures.add(executorService.submit(() -&amp;gt; {
        try {
            // 실제 재고 감소 로직 실행 (재시도 로직 포함)
            // 각 스레드는 재고를 1개씩 감소시키려고 시도
            stockRetryFacade.decreaseWithRetry(
                StockFixture.decreaseRequest(product, 1)
            );

        } catch (InterruptedException e) {
            // InterruptedException 발생 시
            // 1. 현재 스레드의 인터럽트 상태를 복원
            Thread.currentThread().interrupt();

            // 2. RuntimeException으로 감싸서 다시 던짐
            //    (Runnable은 checked exception을 던질 수 없기 때문)
            throw new RuntimeException(e);

        } finally {
            // 작업 성공/실패 여부와 관계없이 항상 실행
            // countDownLatch를 1 감소시켜 &quot;이 스레드는 작업 완료했다&quot;고 알림
            countDownLatch.countDown();
        }
    }));
}

// 모든 스레드가 countDown()을 호출할 때까지 메인 스레드를 여기서 대기
// countDownLatch가 0이 될 때까지 블로킹
// &amp;rarr; 100개 스레드가 모두 작업을 완료하면 다음으로 진행
countDownLatch.await();

// Future 리스트를 순회하면서 각 스레드의 실행 결과 확인
for (Future&amp;lt;?&amp;gt; future : futures) {
    // future.get(): 해당 스레드가 완료될 때까지 대기하고 결과를 가져옴
    // 스레드 내부에서 예외가 발생했다면 여기서 ExecutionException 발생
    // &amp;rarr; 테스트 실패로 처리됨
    future.get();
}

// 스레드 풀 종료 (더 이상 새 작업을 받지 않음)
// 리소스 정리를 위해 항상 호출해야 함
executorService.shutdown();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Future 가 필요한 이유는 뭘까용?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Future 가 없으면 스레드 내부 예외가 숨겨진다.&lt;/li&gt;
&lt;li&gt;예를 들어 :&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771936557318&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void test() {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    CountDownLatch latch = new CountDownLatch(2);

    // 스레드 1
    executor.submit(() -&amp;gt; {
        try {
            throw new RuntimeException(&quot;에러!&quot;);  // &amp;larr; 예외 발생
        } finally {
            latch.countDown();
        }
    });

    // 스레드 2
    executor.submit(() -&amp;gt; {
        try {
            System.out.println(&quot;정상 실행&quot;);
        } finally {
            latch.countDown();
        }
    });

    latch.await();
    executor.shutdown();
    
    // 테스트는 통과! (예외를 몰라서)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1771935981096&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;스레드 1: 성공 (재고 99)
스레드 2: 성공 (재고 98)
...
스레드 50: NullPointerException 발생! &amp;larr; 하지만 테스트는 계속 진행
...
스레드 100: 성공

결과: 재고가 50 (50번만 성공)

하지만 assertEquals(0, stock.getQuantity()) 에서 실패
&amp;rarr; &quot;왜 50이지?&quot; 원인을 알 수 없음!&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Future 를 사용하면 스레드 내부 예외가 명확하게 드러난다.&lt;/li&gt;
&lt;li&gt;예외를 객체에 저장해 get()을 통해 호출 가능하기 때문에 스레드 안에서 발생한 예외를 호출한 쪽에서 확실하게 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;예를 들어 :&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771936508850&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void test() throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    CountDownLatch latch = new CountDownLatch(2);
    List&amp;lt;Future&amp;lt;?&amp;gt;&amp;gt; futures = new ArrayList&amp;lt;&amp;gt;();

    // 스레드 1
    futures.add(executor.submit(() -&amp;gt; {
        try {
            throw new RuntimeException(&quot;에러!&quot;);
        } finally {
            latch.countDown();
        }
    }));

    // 스레드 2
    futures.add(executor.submit(() -&amp;gt; {
        try {
            System.out.println(&quot;정상 실행&quot;);
        } finally {
            latch.countDown();
        }
    }));

    latch.await();

    // Future로 예외 확인
    for (Future&amp;lt;?&amp;gt; future : futures) {
        future.get();  // &amp;larr; ExecutionException 발생! 테스트 실패
    }

    executor.shutdown();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 왜 자료 구조를 ArrayList 로 썼을까용?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순차 접근이 주 목적&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771938339343&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 순차적으로 모든 Future를 확인
for (Future&amp;lt;?&amp;gt; future : futures) {
    future.get();  // 하나씩 차례대로 확인
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 인덱스 기반 빠른 접근 : O(1)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크기를 미리 알수 있고, 데이터 추가에 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771938395717&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int threadCount = 100;  // 크기 고정

// 정확히 100개의 Future가 들어갈 것을 알고 있음
List&amp;lt;Future&amp;lt;?&amp;gt;&amp;gt; futures = new ArrayList&amp;lt;&amp;gt;(threadCount);  // 초기 용량 지정 가능

for (int i = 0; i &amp;lt; threadCount; i++) {
    futures.add(executor.submit(...));  // 끝에 추가만 함
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 데이터 추가(append) : O(1) (평균)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2-1) LinkedList 랑은 무슨 차인데용?&lt;/p&gt;
&lt;pre id=&quot;code_1771938482542&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LinkedList
List&amp;lt;Future&amp;lt;?&amp;gt;&amp;gt; futures = new LinkedList&amp;lt;&amp;gt;();
for (int i = 0; i &amp;lt; 100; i++) {
    futures.add(executor.submit(...));  // O(1)
}
for (Future&amp;lt;?&amp;gt; future : futures) {
    future.get();  // 순회는 괜찮지만 메모리 오버헤드
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 오버헤드(실제 데이터 저장 외에 추가로 소비되는 메모리)가 크다. (각 노드마다 prev, next 포인터)&lt;/li&gt;
&lt;li&gt;인덱스 접근 느림 : O(n)&lt;/li&gt;
&lt;li&gt;캐시 지역성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771938906149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ArrayList: 연속된 메모리 &amp;rarr; CPU 캐시 히트율 높음
for (Future&amp;lt;?&amp;gt; future : futures) {
    future.get();  // &amp;larr; 빠름
}

// LinkedList: 흩어진 메모리 &amp;rarr; CPU 캐시 미스 많음
for (Future&amp;lt;?&amp;gt; future : futures) {
    future.get();  // &amp;larr; 상대적으로 느림
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 실행 흐름&lt;/p&gt;
&lt;pre id=&quot;code_1771935375596&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## 실행 흐름

1. 스레드 풀 생성 (100개)
   
2. 100개 작업 제출 (각각 재고 1씩 감소)
   
3. 모든 작업 동시 실행 (경쟁 상태 발생 가능)
   
4. countDownLatch.await() - 모든 작업 완료 대기
   
5. future.get() - 각 스레드 예외 확인
   
6. DB 조회 - 최종 재고 확인
   
7. assertEquals(0, ...) - 재고가 정확히 0인지 검증&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;then&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;then : 결과 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) DB에서 최신 재고 데이터 조회&lt;/p&gt;
&lt;pre id=&quot;code_1771935085130&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stock updatedStock = jpaStockRepository
            .findByProductId(product.getProductId())  // productId로 재고 조회
            .orElseThrow();  // 없으면 예외 발생&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@BeforeEach에서 생성한 stock 객체가 아닌, DB에서 다시 가져와야 함!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 검증&lt;/p&gt;
&lt;pre id=&quot;code_1771935126762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;assertEquals(0, updatedStock.getQuantity());&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;100개 스레드가 각각 1씩 감소시켰으므로 초기 재고 100 - (100번 &amp;times; 1씩 감소) = 0이어야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;만약 동시성 제어가 제대로 안 되었다면 0보다 큰 값이 나올 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;핵심 개념 정리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ExecutorService (&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/p&gt;
&lt;pre id=&quot;code_1771935242098&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedThreadPool(100);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 작업을 병렬로 실행하기 위한 스레드 풀&lt;/li&gt;
&lt;li&gt;newFixedThreadPool(100): 최대 100개의 작업을 동시에 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. CountDownLatch&lt;/p&gt;
&lt;pre id=&quot;code_1771935260042&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CountDownLatch latch = new CountDownLatch(100);
latch.countDown();  // -1 감소
latch.await();      // 0이 될 때까지 대기&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 스레드의 작업 완료를 동기화&lt;/li&gt;
&lt;li&gt;카운터가 0이 되면 대기 중인 스레드가 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Future (&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/p&gt;
&lt;pre id=&quot;code_1771935305126&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Future&amp;lt;?&amp;gt; future = executorService.submit(...);
future.get();  // 결과 대기 + 예외 확인&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Java 표준 라이브러리&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;비동기 작업의 결과를 나타내는 객체 &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;get() :&lt;/span&gt;&lt;span&gt; 작업 완료 대기 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;+&lt;/span&gt;&lt;span&gt; 예외가 있으면 던짐&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Java 8 이후부터는 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;CompletableFuture 사용을 권장한다고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Oracle Docs : Future&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771937023670&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Future (Java SE 21 &amp;amp; JDK 21)&quot; data-og-description=&quot;Type Parameters: V - The result type returned by this Future's get method All Known Subinterfaces: RunnableFuture , RunnableScheduledFuture , ScheduledFuture All Known Implementing Classes: CompletableFuture, CountedCompleter, ForkJoinTask, FutureTask, Rec&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html&quot; data-og-url=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Future (Java SE 21 &amp;amp; JDK 21)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Type Parameters: V - The result type returned by this Future's get method All Known Subinterfaces: RunnableFuture , RunnableScheduledFuture , ScheduledFuture All Known Implementing Classes: CompletableFuture, CountedCompleter, ForkJoinTask, FutureTask, Rec&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Oracle Docs : ExecutorService&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771939255972&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ExecutorService (Java Platform SE 8 )&quot; data-og-description=&quot;An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks. An ExecutorService can be shut down, which will cause it to reject new tasks. Two different methods are p&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ExecutorService (Java Platform SE 8 )&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks. An ExecutorService can be shut down, which will cause it to reject new tasks. Two different methods are p&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) Oracle Docs : CountDownLatch&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771939297283&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;CountDownLatch (Java Platform SE 8 )&quot; data-og-description=&quot;A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. A CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invo&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;CountDownLatch (Java Platform SE 8 )&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. A CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invo&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/527</guid>
      <comments>https://annovation.tistory.com/527#entry527comment</comments>
      <pubDate>Tue, 24 Feb 2026 21:17:09 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] Optimistic Lock (낙관적 락) 재시도 로직</title>
      <link>https://annovation.tistory.com/526</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Optimistic&amp;nbsp;Lock&amp;nbsp;(낙관적&amp;nbsp;락)&amp;nbsp;재시도&amp;nbsp;로직&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Optimistic Lock (낙관적 락) 에서는 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;실패했을 경우 재시도를 해야하는데,&lt;/span&gt;&amp;nbsp;아래 다양한 방법들을 통해 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Spring Retry&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Facade 패턴&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771812457694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class StockRetryFacade {

    private final StockService stockService; // @Transactional 메서드 가진 서비스

    public StockResult decreaseWithRetry(StockRequests.Decrease request)
            throws InterruptedException {
        int maxRetries = 100;
        int attempt = 0;

        while (true) {
            try {
                return stockService.decreaseStock(request); // 1회 시도 (새 트랜잭션)
            } catch (OptimisticLockingFailureException e) {
                if (++attempt &amp;gt;= maxRetries) {
                    throw e;
                }
                Thread.sleep(50);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;재시도 구현 시 검토해야 할 4가지 원칙&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;낙관적 락에서 발생하는 Conflict는 시스템 장애가 아니다. &lt;/span&gt;오히려 &lt;b&gt;동시성 환경에서 자연스럽게 발생하는 정상적인 제어 흐름&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다. &lt;/span&gt;따라서 모든 충돌을 '에러'로 취급해 무조건 재시도하는 것은 올바른 전략이 아니다. 서비스 특성과 트래픽 패턴에 맞는 재시도 설계가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Fresh Read의 원칙 (최신 데이터 재조회)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재시도 시에는 반드시 &lt;b&gt;Primary DB에서 최신 버전 데이터를 다시 조회&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;메모리에 로드되어 있던 기존 객체를 그대로 들고 재시도하는 것은 무의미한 연쇄 충돌을 야기하고 CPU 자원을 낭비한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771941064823&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;충돌 발생
   &amp;darr;
Primary DB에서 최신 상태 재조회
   &amp;darr;
비즈니스 로직 재계산
   &amp;darr;
다시 CAS 시도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ CAS 란?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Compare And Swap 의 약자로, &lt;span&gt;현재 값이 expected 값과 같으면 &lt;/span&gt;&lt;span&gt;새로운 값으로 교체 &lt;/span&gt;&lt;span&gt;아니면 &lt;/span&gt;&lt;span&gt;아무것도 하지 않고 실패&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;예를 들면 :&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771941142020&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE character_state
SET current_sal = ?, update_version = update_version + 1
WHERE character_id = ?
  AND update_version = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;지수 백오프와 지터&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;충돌 직후 즉시 재시도하면 또다시 충돌할 가능성이 매우 높다.&lt;/li&gt;
&lt;li&gt;예를 들면 :&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772089427891&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 모두 같은 값을 읽음

A &amp;rarr; SAL=1000, version=1

B &amp;rarr; SAL=1000, version=1

C &amp;rarr; SAL=1000, version=1

2. A가 먼저 성공

UPDATE ... WHERE version=1

✔ 성공

SAL = 1100

version = 2

3. 나머지는 실패

여기서 B와 C가 즉시 재시도를 한다면?

4. B와 C가 바로 같은 값을 다시 읽음

B &amp;rarr; SAL=1100, version=2

C &amp;rarr; SAL=1100, version=2

5. 앞선 상황과 같이 B와 C 중 하나만 성공하고 실패한 요청은 다시 데이터를 읽고 또 동시에 업데이트를 시도하는 상황 반복

6. 따라서 실패한 요청을 흩어놓으면 또다시 동시에 부딪히는 상황을 예방할 수 있다.

B &amp;rarr; 50ms 기다림

C &amp;rarr; 120ms 기다림&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;따라서 재시도 간격을 2^n&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;형태로 늘리는 지수 백오프를 적용하고, 여기에 무작위 값인 지터(Jitter)를 섞어 요청 시점을 분산시켜야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771941486230&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;delay = base * 2^n + random_jitter&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들면 :&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;시도 횟수&lt;/td&gt;
&lt;td&gt;대기 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1회&lt;/td&gt;
&lt;td&gt;50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2회&lt;/td&gt;
&lt;td&gt;100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3회&lt;/td&gt;
&lt;td&gt;200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4회&lt;/td&gt;
&lt;td&gt;400ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이는 수많은 요청이 한꺼번에 몰리는 현상을 방지하여 DB의 병목을 막아주는 핵심 기술이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;멱등성(Idempotency) 보장&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 내부 재시도 (OptimisticLockException 이후 retry)가 중복 처리되거나, 네트워크 이슈로 응답 받지 못한 클라이언트가 재요청을 보낼 경우, 동일한 비즈니스 요청이 2번 이상 실행될 가능성이 생긴다.&lt;/li&gt;
&lt;li&gt;즉, Optimistic Lock 은 동시성 제어만 보장할 뿐, 중복 실행 방지는 보장하지 않는다.&lt;/li&gt;
&lt;li&gt;이 경우, 클라이언트가 모든 요청에 대한 고유한 ID인 request_id를 생성하고 이를 기반으로 멱등성 테이블을 운용하여, 동일한 요청에 대해서는 실제 DB 쓰기 없이 이전의 성공 결과를 반환하는 안전장치가 병행되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772093651869&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE idempotency (
    request_id VARCHAR(64) PRIMARY KEY,
    response_payload JSON,
    created_at DATETIME
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 해당 테이블에 동일한 request_id가 존재한다면 이미 처리된 요청이고, 존재하지 않으면 해당 요청은 처음 처리하는 것임을 나타낸다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❗️멱등성(Idempotency)&amp;nbsp; 이란?&lt;br /&gt;&lt;br /&gt;같은 요청을 여러 번 보내도 결과가 한 번 보낸 것과 동일하게 유지되는 성질이다.&lt;br /&gt;어떤 연산을 한 번 수행하든 여러 번 수행하든 최종 상태가 동일하면 멱등하다고 한다. (&lt;a href=&quot;https://docs.tosspayments.com/blog/what-is-idempotency&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 자료&lt;/a&gt;)&lt;br /&gt;&lt;br /&gt;예를 들어, HTTP 메서드를 기준으로 보면 GET은 여러 번 호출해도 같은 결과가 돌아오고, 리소스에 변화를 일으키지 않기 때문에 멱등성이 보장된 메서드이다.&lt;br /&gt;&lt;br /&gt;PUT /users/1로 이름을 '홍길동'으로 설정하는 요청은 여러 번 보내도 결과는 동일 &amp;rarr; 멱등&lt;br /&gt;POST /orders는 호출할 때마다 주문이 새로 생성 &amp;rarr; 멱등하지 않음&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;최대 재시도 횟수 제한&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 락에서의 재시도는 일시적인 충돌을 완화하기 위한 전략일 뿐, 무한히 반복되어서는 안된다.&lt;/li&gt;
&lt;li&gt;충돌이 일정 횟수 이상 반복된다는 것은 단순한 타이밍 문제를 넘어, 특정 데이터에 과도한 경합(Hot Row Contention)이 발생하고 있다는 신호일 수 있다.&lt;/li&gt;
&lt;li&gt;예를 들어 3~5회 이상 충돌이 반복된다면 이는 단순한 일시적 충돌이 아니라 설계적 병목 가능성을 의심해야 할 상황이다.&lt;/li&gt;
&lt;li&gt;이 경우, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;사용자에게 명확한 에러를 반환하거나, 비즈니스 로직에 따라 비관적 락 등으로의 전환을 검토해야 하는 신호일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 뱅크샐러드 블로그 : 뱅크샐러드가 게임을 만들 때 데이터 정합성을 유지하는 법 (feat. 낙관적 락)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.banksalad.com/tech/banksalad-optimistic-lock/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.banksalad.com/tech/banksalad-optimistic-lock/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771831976046&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;뱅크샐러드가 게임을 만들 때 데이터 정합성을 유지하는 법 (feat. 낙관적 락) | 뱅크샐러드&quot; data-og-description=&quot;안녕하세요 금융쇼핑 PA&amp;hellip;&quot; data-og-host=&quot;blog.banksalad.com&quot; data-og-source-url=&quot;https://blog.banksalad.com/tech/banksalad-optimistic-lock/&quot; data-og-url=&quot;https://blog.banksalad.com/tech/banksalad-optimistic-lock/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bmaNsa/dJMb87NTgB8/IGkZYDlNH8H3SWKH3sUK71/img.jpg?width=2400&amp;amp;height=1600&amp;amp;face=0_0_2400_1600,https://scrap.kakaocdn.net/dn/ru569/dJMb8RROLWd/FpZTFGIGVkjkY4kWoyhjWK/img.png?width=650&amp;amp;height=366&amp;amp;face=0_0_650_366,https://scrap.kakaocdn.net/dn/tQIzB/dJMb8865KJ2/hk05oPnNp22ZiBiZNdBnn0/img.png?width=650&amp;amp;height=366&amp;amp;face=0_0_650_366&quot;&gt;&lt;a href=&quot;https://blog.banksalad.com/tech/banksalad-optimistic-lock/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.banksalad.com/tech/banksalad-optimistic-lock/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bmaNsa/dJMb87NTgB8/IGkZYDlNH8H3SWKH3sUK71/img.jpg?width=2400&amp;amp;height=1600&amp;amp;face=0_0_2400_1600,https://scrap.kakaocdn.net/dn/ru569/dJMb8RROLWd/FpZTFGIGVkjkY4kWoyhjWK/img.png?width=650&amp;amp;height=366&amp;amp;face=0_0_650_366,https://scrap.kakaocdn.net/dn/tQIzB/dJMb8865KJ2/hk05oPnNp22ZiBiZNdBnn0/img.png?width=650&amp;amp;height=366&amp;amp;face=0_0_650_366');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;뱅크샐러드가 게임을 만들 때 데이터 정합성을 유지하는 법 (feat. 낙관적 락) | 뱅크샐러드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요 금융쇼핑 PA&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.banksalad.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 토스 블로그 : 멱등성이 뭔가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.tosspayments.com/blog/what-is-idempotency&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.tosspayments.com/blog/what-is-idempotency&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772094738508&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;멱등성이 뭔가요? | 토스페이먼츠 개발자센터&quot; data-og-description=&quot;생소한 표현이지만 알고 보면 쉬워요. 멱등성에 대해 이해하고 API를 멱등하게 제공하기 위한 방법도 함께 알아봐요.&quot; data-og-host=&quot;docs.tosspayments.com&quot; data-og-source-url=&quot;https://docs.tosspayments.com/blog/what-is-idempotency&quot; data-og-url=&quot;https://docs.tosspayments.com/blog/what-is-idempotency&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1t6B6/dJMb8TB6MUh/YGjOFZmnGzXLkGXJSBPQT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/caoklu/dJMb82MAgnE/LZvVfravESjdezgdGyFjUk/img.png?width=4659&amp;amp;height=2367&amp;amp;face=0_0_4659_2367&quot;&gt;&lt;a href=&quot;https://docs.tosspayments.com/blog/what-is-idempotency&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.tosspayments.com/blog/what-is-idempotency&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1t6B6/dJMb8TB6MUh/YGjOFZmnGzXLkGXJSBPQT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/caoklu/dJMb82MAgnE/LZvVfravESjdezgdGyFjUk/img.png?width=4659&amp;amp;height=2367&amp;amp;face=0_0_4659_2367');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;멱등성이 뭔가요? | 토스페이먼츠 개발자센터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;생소한 표현이지만 알고 보면 쉬워요. 멱등성에 대해 이해하고 API를 멱등하게 제공하기 위한 방법도 함께 알아봐요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.tosspayments.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/526</guid>
      <comments>https://annovation.tistory.com/526#entry526comment</comments>
      <pubDate>Mon, 23 Feb 2026 23:53:49 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] DB Lock : Optimistic Lock (낙관적 락) - 실습</title>
      <link>https://annovation.tistory.com/525</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Optimistic Lock (낙관적 락)&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;Optimistic Lock (낙관적 락) 이란?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/509&quot;&gt;https://annovation.tistory.com/509&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771488485331&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 (실습중..)&quot; data-og-description=&quot;동시성 문제 (Race Condition) 해결 방법레이스 컨디션을 해결하는 방법에는 크게 3가지 관점이 있다.첫번째는 Java에서 지원하는 Synchronized 방법으로 해결하기두번째는 DB가 제공하는 Lock 을 이용하&quot; data-og-host=&quot;annovation.tistory.com&quot; data-og-source-url=&quot;https://annovation.tistory.com/509&quot; data-og-url=&quot;https://annovation.tistory.com/509&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bX10OE/dJMb86nT4ES/KK5CsiUEzGktYrbneba7i0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/m6j0e/dJMb86OYl4k/xzIBL8FcCaMaDfQVurpLK0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dBQd55/dJMb8TB57dv/6W6G39YgpNYmVLGtE90wX0/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/509&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://annovation.tistory.com/509&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bX10OE/dJMb86nT4ES/KK5CsiUEzGktYrbneba7i0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/m6j0e/dJMb86OYl4k/xzIBL8FcCaMaDfQVurpLK0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dBQd55/dJMb8TB57dv/6W6G39YgpNYmVLGtE90wX0/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 (실습중..)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;동시성 문제 (Race Condition) 해결 방법레이스 컨디션을 해결하는 방법에는 크게 3가지 관점이 있다.첫번째는 Java에서 지원하는 Synchronized 방법으로 해결하기두번째는 DB가 제공하는 Lock 을 이용하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;annovation.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;동시성 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 전 코드&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockController&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771812901168&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Operation(summary = &quot;재고 감소 API&quot;, description = &quot;상품 주문시 재고가 감소한다.&quot;)
@PutMapping
public ResponseEntity&amp;lt;ApiResponse&amp;lt;StockResponse&amp;gt;&amp;gt; decreaseStock(
        @Valid @RequestBody StockRequests.Decrease request) {

    // 재고 감소 로직 호출
    StockResult result = stockService.decreaseStock(request);

    return ResponseEntity.status(HttpStatus.OK)
            .body(ApiResponse.success(StockResponse.from(result)));
}

@Operation(summary = &quot;재고 복원 API&quot;, description = &quot;상품 취소시 재고가 복원된다.&quot;)
@PutMapping(&quot;/restore&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;StockResponse&amp;gt;&amp;gt; restoreStock(
        @Valid @RequestBody StockRequests.Restore request) {

    // 재고 복원 로직 호출
    StockResult result = stockService.restoreStock(request);

    return ResponseEntity.status(HttpStatus.OK)
            .body(ApiResponse.success(StockResponse.from(result)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockServiceImpl.decreaseStock&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771488483888&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
@Transactional
public StockResult decreaseStock(StockRequests.Decrease request) {

    Product product = getProductOrThrow(request.productId());

    Stock stock = getStockOrThrow(request.productId());

    stock.decreaseQuantity(request.quantity());

    return StockResult.from(stock, product.getName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stock&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771488483889&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void decreaseQuantity(int decreaseQuantity) {
    validateDecreaseQuantity(decreaseQuantity);

    if (this.quantity &amp;lt; decreaseQuantity) {
        throw new GlobalException(INSUFFICIENT_STOCK);
    }

    this.quantity -= decreaseQuantity;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;JpaStockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771678095446&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface JpaStockRepository extends JpaRepository&amp;lt;Stock, UUID&amp;gt; {

    Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;StockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771678109381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StockRepository {

	Stock save(Stock stock);

	Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;StockRepositoryAdapter&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771678120415&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class StockRepositoryAdapter implements StockRepository {

	private final JpaStockRepository jpaStockRepository;

	@Override
	public Stock save(Stock stock) {
		return jpaStockRepository.save(stock);
	}

	@Override
	public Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId) {
		return jpaStockRepository.findByProductId(productId);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockCurrencyTest&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771678199881&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    stockServiceImpl.decreaseStock(
            StockFixture.decreaseRequest(product, 1)
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 전 테스트 코드 결과&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-19 at 12.25.02 AM.png&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq27Fu/dJMcabwoxaI/K8PE5Rk9TkjltkdLp9GnU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq27Fu/dJMcabwoxaI/K8PE5Rk9TkjltkdLp9GnU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq27Fu/dJMcabwoxaI/K8PE5Rk9TkjltkdLp9GnU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq27Fu%2FdJMcabwoxaI%2FK8PE5Rk9TkjltkdLp9GnU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2154&quot; height=&quot;560&quot; data-filename=&quot;Screenshot 2026-02-19 at 12.25.02 AM.png&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고 100개에 대해 1개씩 100번 동시 차감했을 때 0이 되어야 하는데, 실제로는 88이 남아 동시성 제어가 제대로 되지 않아 일부 차감이 반영되지 않은 상태에서 발생한 AssertionFailedError가 발생했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 한 코드&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockController&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771812973822&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PutMapping
public ResponseEntity&amp;lt;ApiResponse&amp;lt;StockResponse&amp;gt;&amp;gt; decreaseStock(
	@Valid @RequestBody StockRequests.Decrease request) {
    
    StockResult result = stockRetryFacade.decreaseWithRetry(request);
    
    return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.success(StockResponse.from(result)));
}

@PutMapping(&quot;/restore&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;StockResponse&amp;gt;&amp;gt; restoreStock(
	@Valid @RequestBody StockRequests.Restore request) {
    
    StockResult result = stockRetryFacade.restoreWithRetry(request);
    
    return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.success(StockResponse.from(result)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 낙관적 락 재시도 로직을 사용할 수 있도록 Facade 클래스를 Controller에서 반영하도록 수정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockServiceImpl&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771677967816&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Stock getStockWithLockOrThrow(UUID productId) {
        return stockRepository
                .findByProductWithOptimisticLock(productId)
                .orElseThrow(() -&amp;gt; new GlobalException(STOCK_NOT_FOUND));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;➡️&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Lock 조회 메서드 추가&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stock&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771597817540&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@Table(name = &quot;p_stock&quot;)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = &quot;stock_id&quot;, nullable = false)
    private UUID stockId;

    @Column(name = &quot;product_id&quot;, nullable = false)
    private UUID productId;

    @Column(name = &quot;company_id&quot;, nullable = false)
    private UUID companyId;

    @Column(name = &quot;hub_id&quot;, nullable = false)
    private UUID hubId;

    @Column(name = &quot;quantity&quot;, nullable = false)
    private int quantity;

    @Version
    private Long version;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ Stock Entity 에 Version 필드 추가&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JpaStockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771597952470&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface JpaStockRepository extends JpaRepository&amp;lt;Stock, UUID&amp;gt; {

    Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);

    @Lock(LockModeType.OPTIMISTIC)
    @Query(&quot;SELECT s FROM Stock s WHERE s.productId = :productId&quot;)
    Optional&amp;lt;Stock&amp;gt; findByProductWithOptimisticLock(@Param(&quot;productId&quot;) UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771598022206&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StockRepository {

	Stock save(Stock stock);

	Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);
	
	Optional&amp;lt;Stock&amp;gt; findByProductWithOptimisticLock(UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockRepositoryAdapter&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771598033527&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class StockRepositoryAdapter implements StockRepository {

    private final JpaStockRepository jpaStockRepository;

    @Override
    public Stock save(Stock stock) {
        return jpaStockRepository.save(stock);
    }

    @Override
    public Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId) {
        return jpaStockRepository.findByProductId(productId);
    }

    @Override
    public Optional&amp;lt;Stock&amp;gt; findByProductWithOptimisticLock(UUID productId) {
        return jpaStockRepository.findByProductWithOptimisticLock(productId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;applicaiton/service/StockRetryFacade.java 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771597740697&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class StockRetryFacade {

    private final StockService stockService; // @Transactional 메서드 가진 서비스

    public StockResult decreaseWithRetry(StockRequests.Decrease request)
            throws InterruptedException {
        int maxRetries = 100;
        int attempt = 0;

        while (true) {
            try {
                return stockService.decreaseStock(request); // 1회 시도 (새 트랜잭션)
            } catch (OptimisticLockingFailureException e) {
                if (++attempt &amp;gt;= maxRetries) {
                    throw e;
                }
                Thread.sleep(50);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ Optimistic Lock은 실패했을 경우 재시도를 해야하기 때문에&lt;span&gt; 이를 구현하기 위한 방법 중 강의에 소개된&amp;nbsp;&lt;/span&gt;facade(퍼사드)를 통해 생성하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockCurrencyTest&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771678403110&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;재고 감소 - 100개 동시 요청 시 정확히 차감&quot;)
void decreaseStock_when100ConcurrentRequests_thenSuccess() throws Exception {

    // given
    int threadCount = 100;

    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

    CountDownLatch countDownLatch = new CountDownLatch(threadCount);

    List&amp;lt;Future&amp;lt;?&amp;gt;&amp;gt; futures = new ArrayList&amp;lt;&amp;gt;();

	// when
    for (int i = 0; i &amp;lt; threadCount; i++) {
        futures.add(executorService.submit(() -&amp;gt; {
            try {
                stockRetryFacade.decreaseWithRetry(StockFixture.decreaseRequest(product, 1));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            } finally {
                countDownLatch.countDown();
            }
        }));
    }

    countDownLatch.await();

    for (Future&amp;lt;?&amp;gt; future : futures) {
        future.get(); // 스레드 내부 예외를 테스트 실패로 반영
    }

    executorService.shutdown();

    // then
    Stock updatedStock = jpaStockRepository
            .findByProductId(product.getProductId())
            .orElseThrow();

    assertEquals(0, updatedStock.getQuantity());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➡️&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;테스트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;StockServiceImpl&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;StockRetryFacade&lt;/span&gt;를 주입해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;decreaseWithRetry&lt;/span&gt;를 호출하도록 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 후 테스트 코드 결과&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-21 at 10.15.00 PM.png&quot; data-origin-width=&quot;2882&quot; data-origin-height=&quot;1622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUQ0Yg/dJMcahDpAyo/BKl1iZ1fJ6URcSO17d9oW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUQ0Yg/dJMcahDpAyo/BKl1iZ1fJ6URcSO17d9oW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUQ0Yg/dJMcahDpAyo/BKl1iZ1fJ6URcSO17d9oW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUQ0Yg%2FdJMcahDpAyo%2FBKl1iZ1fJ6URcSO17d9oW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2882&quot; height=&quot;1622&quot; data-filename=&quot;Screenshot 2026-02-21 at 10.15.00 PM.png&quot; data-origin-width=&quot;2882&quot; data-origin-height=&quot;1622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/525</guid>
      <comments>https://annovation.tistory.com/525#entry525comment</comments>
      <pubDate>Fri, 20 Feb 2026 23:29:16 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] Trouble Shooting : Pessimistic Lock 적용 오류</title>
      <link>https://annovation.tistory.com/524</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;진행 환경&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;진행 환경&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Java 17&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Spring Boot 3.5.7&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;MySQL 8.0.44&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Gradle&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pessimistic Lock (낙관적 락) 적용을 위해&lt;span style=&quot;color: #333333;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;@Lock(LockModeType.PESSIMISTIC_WRITE)&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;추가 후 아래와 같은 오류 발생&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-19 at 5.11.20 PM.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZwHhh/dJMcaduaKqb/elNw332RvpbEAEf1lPAHtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZwHhh/dJMcaduaKqb/elNw332RvpbEAEf1lPAHtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZwHhh/dJMcaduaKqb/elNw332RvpbEAEf1lPAHtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZwHhh%2FdJMcaduaKqb%2FelNw332RvpbEAEf1lPAHtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2100&quot; height=&quot;622&quot; data-filename=&quot;Screenshot 2026-02-19 at 5.11.20 PM.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;➡️ &lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;TransactionRequiredException: Query requires transaction be in progress&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;background-color: #f6e199; color: #222222; text-align: start;&quot;&gt;문제 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;증상&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;PESSIMISTIC_WRITE&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;락 쿼리를 트랜잭션 없이 실행해서 난 오류&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : 테스트 코드에서 단순히 재고 데이터를 읽어오는 작업이더라도 트랜잭션이 걸려있어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 테스트 코드에서 레이스 컨디션 검증 포인트는 아래 stockServiceImpl 에서 decreaseStock 을 통해 동시 호출 하는 구간이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771563194012&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; threadCount; i++) {
            executorService.submit(() -&amp;gt; {

                try {
                    stockServiceImpl.decreaseStock(
                            StockFixture.decreaseRequest(product, 1)
                    );
                } finally {
                    countDownLatch.countDown();
                }
            });
        }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그런데, &lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;JpaStockRepository.findByProductId&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;에&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;@Lock(PESSIMISTIC_WRITE) 을 통해 동시성 처리를 하고 있기 때문에 findByProductId 와 같은 단순 조회 로직이더라도 &lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;가 아니라 락 쿼리(&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;FOR UPDATE&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;)로 실행되고 이는 트랜잭션 환경이 필수적이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771563463102&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface JpaStockRepository extends JpaRepository&amp;lt;Stock, UUID&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 StockServiceImpl 메서드들은 트랜잭션이 걸려있지만, 테스트 코드에서는 StockServiceImpl 을 거치지 않고 테스트가 Repository를 직접 호출 했기 때문에 해당 호출 자체에는 트랜잭션이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771564531035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stock updatedStock = jpaStockRepository
                .findByProductId(product.getProductId())
                .orElseThrow();

        assertEquals(0, updatedStock.getQuantity());&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방안 검토&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 1 : Lock 없는 메서드로 읽기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 새롭게 발견한 문제점 : 단순 조회 API 까지 Lock 조회가 되어 성능이 떨어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1771584775953&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
@Transactional(readOnly = true)
public StockResult getStockByProductId(UUID productId) {

    Product product = getProductOrThrow(productId);

    Stock stock = getStockOrThrow(productId);

    return StockResult.from(stock, product.getName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서 조회용으로 Lock 이 없는 메서드를 따로 분리하여 설계한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771591883215&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface JpaStockRepository extends JpaRepository&amp;lt;Stock, UUID&amp;gt; {

    Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId); // 일반 조회

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select s from Stock s where s.productId = :productId&quot;)
    Optional&amp;lt;Stock&amp;gt; findByProductIdForUpdate(@Param(&quot;productId&quot;) UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;결과&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;테스트 코드 정상적으로 통과!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-20 at 10.28.16 PM.png&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9SsHX/dJMcafr3yCp/cyOe9KPkruI2NK0R8PWwrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9SsHX/dJMcafr3yCp/cyOe9KPkruI2NK0R8PWwrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9SsHX/dJMcafr3yCp/cyOe9KPkruI2NK0R8PWwrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9SsHX%2FdJMcafr3yCp%2FcyOe9KPkruI2NK0R8PWwrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2876&quot; height=&quot;1636&quot; data-filename=&quot;Screenshot 2026-02-20 at 10.28.16 PM.png&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;1636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>왜안되지?</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/524</guid>
      <comments>https://annovation.tistory.com/524#entry524comment</comments>
      <pubDate>Thu, 19 Feb 2026 16:33:27 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] DB Lock : Pessimistic Lock (비관적 락) - 실습</title>
      <link>https://annovation.tistory.com/523</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Pessimistic Lock (비관적 락)&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;Pessimistic Lock (비관적 락) 이란?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/509&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://annovation.tistory.com/509&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771428628703&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 (실습중..)&quot; data-og-description=&quot;동시성 문제 (Race Condition) 해결 방법레이스 컨디션을 해결하는 방법에는 크게 3가지 관점이 있다.첫번째는 Java에서 지원하는 Synchronized 방법으로 해결하기두번째는 DB가 제공하는 Lock 을 이용하&quot; data-og-host=&quot;annovation.tistory.com&quot; data-og-source-url=&quot;https://annovation.tistory.com/509&quot; data-og-url=&quot;https://annovation.tistory.com/509&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vtJsQ/dJMb8YXHV5s/WNukZySJfEyp8iqyvOv28K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/LaRlD/dJMb8T9VUPd/dV5kNKlNrRzzswKoPUMh81/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/DDZFB/dJMb8YpRTsH/ZTBz9bYr4GT977HbUcGlX0/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/509&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://annovation.tistory.com/509&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vtJsQ/dJMb8YXHV5s/WNukZySJfEyp8iqyvOv28K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/LaRlD/dJMb8T9VUPd/dV5kNKlNrRzzswKoPUMh81/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/DDZFB/dJMb8YpRTsH/ZTBz9bYr4GT977HbUcGlX0/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 (실습중..)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;동시성 문제 (Race Condition) 해결 방법레이스 컨디션을 해결하는 방법에는 크게 3가지 관점이 있다.첫번째는 Java에서 지원하는 Synchronized 방법으로 해결하기두번째는 DB가 제공하는 Lock 을 이용하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;annovation.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;동시성 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 전 코드&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockServiceImpl&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771427824295&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
@Transactional
public StockResult decreaseStock(StockRequests.Decrease request) {

    Product product = getProductOrThrow(request.productId());

    Stock stock = getStockOrThrow(request.productId());

    stock.decreaseQuantity(request.quantity());

    return StockResult.from(stock, product.getName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stock&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771427854727&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void decreaseQuantity(int decreaseQuantity) {
    validateDecreaseQuantity(decreaseQuantity);

    if (this.quantity &amp;lt; decreaseQuantity) {
        throw new GlobalException(INSUFFICIENT_STOCK);
    }

    this.quantity -= decreaseQuantity;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JpaStockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771592777718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface JpaStockRepository extends JpaRepository&amp;lt;Stock, UUID&amp;gt; {

    Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771592797749&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StockRepository {

	Stock save(Stock stock);

	Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockRepositoryAdapter&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771592828080&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class StockRepositoryAdapter implements StockRepository {

	private final JpaStockRepository jpaStockRepository;

	@Override
	public Stock save(Stock stock) {
		return jpaStockRepository.save(stock);
	}

	@Override
	public Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId) {
		return jpaStockRepository.findByProductId(productId);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 전 테스트 코드 결과&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-19 at 12.25.02 AM.png&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq27Fu/dJMcabwoxaI/K8PE5Rk9TkjltkdLp9GnU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq27Fu/dJMcabwoxaI/K8PE5Rk9TkjltkdLp9GnU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq27Fu/dJMcabwoxaI/K8PE5Rk9TkjltkdLp9GnU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq27Fu%2FdJMcabwoxaI%2FK8PE5Rk9TkjltkdLp9GnU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2154&quot; height=&quot;560&quot; data-filename=&quot;Screenshot 2026-02-19 at 12.25.02 AM.png&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고 100개에 대해 1개씩 100번 동시 차감했을 때 0이 되어야 하는데, 실제로는 88이 남아 동시성 제어가 제대로 되지 않아 일부 차감이 반영되지 않은 상태에서 발생한 AssertionFailedError가 발생했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 한 코드&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JpaStockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771593411047&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface JpaStockRepository extends JpaRepository&amp;lt;Stock, UUID&amp;gt; {

    Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select s from Stock s where s.productId = :productId&quot;)
    Optional&amp;lt;Stock&amp;gt; findByProductIdForUpdate(@Param(&quot;productId&quot;) UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️&lt;span style=&quot;color: #333333;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;@Lock(PESSIMISTIC_WRITE)&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;가 붙어서 실행 시점에&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;FOR UPDATE&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;가 추가된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockRepository&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771593537448&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StockRepository {

	Stock save(Stock stock);

	Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId);
	
	Optional&amp;lt;Stock&amp;gt; findByProductIdForUpdate(UUID productId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockRepositoryAdapter&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771593575578&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class StockRepositoryAdapter implements StockRepository {

	private final JpaStockRepository jpaStockRepository;

	@Override
	public Stock save(Stock stock) {
		return jpaStockRepository.save(stock);
	}

	@Override
	public Optional&amp;lt;Stock&amp;gt; findByProductId(UUID productId) {
		return jpaStockRepository.findByProductId(productId);
	}
	
	@Override
	public Optional&amp;lt;Stock&amp;gt; findByProductIdForUpdate(UUID productId) {
		return jpaStockRepository.findByProductIdForUpdate(productId);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockServiceImpl&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771593713428&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Stock getStockForUpdateOrThrow(UUID productId) {
    return stockRepository.findByProductIdForUpdate(productId)
            .orElseThrow(() -&amp;gt; new GlobalException(STOCK_NOT_FOUND));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt; Lock 조회 메서드 추가&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;동시성 처리 후 테스트 코드 결과&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-20 at 10.23.57 PM.png&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2Ntpi/dJMcabb7bCk/oHJVoHDRiOSexpGaF3VIUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2Ntpi/dJMcabb7bCk/oHJVoHDRiOSexpGaF3VIUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2Ntpi/dJMcabb7bCk/oHJVoHDRiOSexpGaF3VIUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2Ntpi%2FdJMcabb7bCk%2FoHJVoHDRiOSexpGaF3VIUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2876&quot; height=&quot;1636&quot; data-filename=&quot;Screenshot 2026-02-20 at 10.23.57 PM.png&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;1636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트가 성공적으로 통과되었다 !&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/523</guid>
      <comments>https://annovation.tistory.com/523#entry523comment</comments>
      <pubDate>Wed, 18 Feb 2026 23:56:03 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] Trouble Shooting : 멀티스레드 재고 감소 통합 테스트 코드 Eureka Server Connection refused</title>
      <link>https://annovation.tistory.com/522</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;진행 환경&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;진행 환경&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Java 17&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Spring Boot 3.5.7&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;MySQL 8.0.44&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;Gradle&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고 감소 시 동시에 100개의 요청을 정확히 차감하는지 테스트 하는 코드에서 오류 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;background-color: #f6e199; color: #222222; text-align: start;&quot;&gt;문제 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;증상&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;org.springframework.web.client.ResourceAccessException: I/O error on GET request for &quot;http://localhost:8761/eureka/apps/&quot;: Connect to http://localhost:8761 failed: Connection refused&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Eureka 서버(8761 포트)에 연결하려고 했는데 해당 포트에 서버가 떠 있지 않아서 거절당한 것&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : 테스트 실행 중 Config Client가 활성화되어 Eureka를 통해 Config Server를 찾으려 함&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@SpringBootTest 가 전체 ApplicationContext를 다 올리고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@SpringBootTest&lt;span&gt;&amp;nbsp;동작 흐름&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;706&quot; data-start=&quot;671&quot;&gt;테스트 클래스에서 @SpringBootTest 발견&lt;/li&gt;
&lt;li data-end=&quot;748&quot; data-start=&quot;707&quot;&gt;Spring Test Framework가 &lt;b&gt;테스트 컨텍스트 생성&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;804&quot; data-start=&quot;749&quot;&gt;SpringApplication.run 을 통해 &lt;b&gt;ApplicationContext 생성&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;854&quot; data-start=&quot;805&quot;&gt;Spring Boot auto-configuration + Test 환경 설정 적용&lt;/li&gt;
&lt;li data-end=&quot;864&quot; data-start=&quot;855&quot;&gt;테스트 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ ApplicationContext 개념 및 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) ApplicationContext 란? (&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1973&quot; data-start=&quot;1892&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1914&quot; data-start=&quot;1892&quot;&gt;Spring Container&lt;/li&gt;
&lt;li data-end=&quot;1946&quot; data-start=&quot;1915&quot;&gt;Bean 정의, 의존성 주입, 라이프사이클 관리 제공&lt;/li&gt;
&lt;li data-end=&quot;1973&quot; data-start=&quot;1947&quot;&gt;환경, 프로퍼티, 메시지, 이벤트 처리 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;It extends the BeanFactory interface, so it provides all the functionality of BeanFactory and more.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Context 란?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Context = &lt;b&gt;환경 + 설정 + Bean들의 집합&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;현재 애플리케이션이 동작하는 설정 환경 전체&lt;/li&gt;
&lt;li&gt;즉, 현재 실행 중인 Spring 컨테이너의 전체 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;A Spring ApplicationContext is essentially a container of beans.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방안 검토&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 1 : 기존 Config Server 트러블슈팅 해결 방안을 적용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/512&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://annovation.tistory.com/512&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771253260424&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[동시성 처리] Trouble Shooting : 테스트 코드 Config Client 오류&quot; data-og-description=&quot;문제 상황  문제 상황Stock 테스트 코드에서 yml 설정으로 config server 를 false 설정 했음에도 관련 오류로 인해 테스트 실행이 안되는 상황 발생  문제 코드 - StockServiceTest.java@ActiveProfiles(&amp;quot;test&amp;quot;)@Sp&quot; data-og-host=&quot;annovation.tistory.com&quot; data-og-source-url=&quot;https://annovation.tistory.com/512&quot; data-og-url=&quot;https://annovation.tistory.com/512&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ckp12Y/dJMb8SpEhX3/yoaCYXiPHhOKCdxr8FxSsK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/p9JRN/dJMb8XkbJYo/SkWlK6gpyYB32ejLNWFiD0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bOBTqV/dJMb8RROf1R/FpmoGck6RI3uSWaC9uBEi1/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/512&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://annovation.tistory.com/512&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ckp12Y/dJMb8SpEhX3/yoaCYXiPHhOKCdxr8FxSsK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/p9JRN/dJMb8XkbJYo/SkWlK6gpyYB32ejLNWFiD0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bOBTqV/dJMb8RROf1R/FpmoGck6RI3uSWaC9uBEi1/img.jpg?width=736&amp;amp;height=736&amp;amp;face=0_0_736_736');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[동시성 처리] Trouble Shooting : 테스트 코드 Config Client 오류&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;문제 상황  문제 상황Stock 테스트 코드에서 yml 설정으로 config server 를 false 설정 했음에도 관련 오류로 인해 테스트 실행이 안되는 상황 발생  문제 코드 - StockServiceTest.java@ActiveProfiles(&quot;test&quot;)@Sp&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;annovation.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : &lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;해당 트러블슈팅으로 인해 기존에 작성해둔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;application-test.yml 을 적용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771253352818&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ActiveProfiles(&quot;test&quot;)
@SpringBootTest
public class StockCurrencyTest {

...(생략)...

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ @ActiveProfiles(&quot;test&quot;) 역할 (&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2737&quot; data-start=&quot;2714&quot;&gt;테스트에서 &lt;b&gt;사용할 프로필을 지정&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2765&quot; data-start=&quot;2738&quot;&gt;지정된 프로필만 적용해서 context를 로딩&lt;/li&gt;
&lt;li data-end=&quot;2788&quot; data-start=&quot;2766&quot;&gt;테스트 환경 분리/커스터마이징에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 여기서 말하는 프로필(profile) 이란?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 다른 환경(dev, test, prod)에 따라 다른 Bean을 등록하기 위한 기능 (&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/environment.html#beans-definition-profiles&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Bean definition profiles provide a way to register different beans for different environments.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ application-test.yml 을 사용하는데 프로필(profile) 개념이 연관되는 이유?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) profile-specific&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application-test.yml 과 같은 형식으로 설정값(Property)을 분리해 환경마다 다른 Bean 을 주입시킬 수 있다. (&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.files.profile-specific&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;You can specify profile-specific configuration files by using the application-{profile}.yml format.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Profile 은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2280&quot; data-start=&quot;2261&quot;&gt;어떤 Bean을 등록할지 결정하면서 동시에&lt;/li&gt;
&lt;li data-end=&quot;2302&quot; data-start=&quot;2281&quot;&gt;어떤 yml 파일을 로드할지 결정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;yml은 설정값이고, profile은 Bean과 설정을 모두 제어하는 환경 스위치다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;참고 자료&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Spring Docs : ActiveProfiles&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771254798142&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;@ActiveProfiles :: Spring Framework&quot; data-og-description=&quot;@ActiveProfiles is an annotation that can be applied to a test class to declare which bean definition profiles should be active when loading an ApplicationContext for an integration test.&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html?utm_source=chatgpt.com&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html?utm_source=chatgpt.com&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;@ActiveProfiles :: Spring Framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;@ActiveProfiles is an annotation that can be applied to a test class to declare which bean definition profiles should be active when loading an ApplicationContext for an integration test.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>왜안되지?</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/522</guid>
      <comments>https://annovation.tistory.com/522#entry522comment</comments>
      <pubDate>Mon, 16 Feb 2026 23:32:52 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] synchronized vs Reentrant Lock - 실습</title>
      <link>https://annovation.tistory.com/521</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;실습&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;synchronized&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annovation.tistory.com/509&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://annovation.tistory.com/509&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771161819666&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;동시성 이슈 해결방법[2] - synchronized&quot; data-og-description=&quot;synchronized로 동시성 이슈를 해결하지 못하는 이유와 @Transactional과의 충돌!&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot; data-og-url=&quot;https://velog.io/@kiteof_park/SpringBoot-동시성-이슈-해결방법2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/birq8p/dJMb8UHLsHs/lEpIkxRJzUKNPqHYmo1qt0/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/bB2T4i/dJMb8XR1TcE/dykuw2tLKkRKCxjIE0tyM1/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/bZLVzY/dJMb8WMlRlK/IZ7OrWXZPAJSPjThujChk1/img.png?width=1024&amp;amp;height=435&amp;amp;face=0_0_1024_435&quot;&gt;&lt;a href=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@kiteof_park/SpringBoot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%952#-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-1---java-synchronized&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/birq8p/dJMb8UHLsHs/lEpIkxRJzUKNPqHYmo1qt0/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/bB2T4i/dJMb8XR1TcE/dykuw2tLKkRKCxjIE0tyM1/img.png?width=764&amp;amp;height=398&amp;amp;face=0_0_764_398,https://scrap.kakaocdn.net/dn/bZLVzY/dJMb8WMlRlK/IZ7OrWXZPAJSPjThujChk1/img.png?width=1024&amp;amp;height=435&amp;amp;face=0_0_1024_435');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;동시성 이슈 해결방법[2] - synchronized&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;synchronized로 동시성 이슈를 해결하지 못하는 이유와 @Transactional과의 충돌!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Reentrant&amp;nbsp;Lock&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 사용법&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771163521275&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final ReentrantLock lock = new ReentrantLock();

public void method() {
    lock.lock();
    try {
        // 비즈니스 로직
    } finally {
        lock.unlock();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HubEleven 프로젝트 적용 실습&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771162578793&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class StockServiceImpl implements StockService {

    private final StockRepository stockRepository;
    private final ProductRepository productRepository;

    private final ReentrantLock lock = new ReentrantLock();

	... (생략) ...

    @Override
    @Transactional
    public StockResult decreaseStock(StockRequests.Decrease request) {

        lock.lock();
        try {
            Product product = getProductOrThrow(request.productId());

            Stock stock = getStockOrThrow(request.productId());

            stock.decreaseQuantity(request.quantity());

            return StockResult.from(stock, product.getName());
        } finally {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 한계 : MSA 와 같은 멀티 서버에서는 무용지물&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 서버 구성 예시&lt;/p&gt;
&lt;pre id=&quot;code_1771163289700&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Server A &amp;rarr; JVM A &amp;rarr; Heap A &amp;rarr; Object A(모니터 A)
Server B &amp;rarr; JVM B &amp;rarr; Heap B &amp;rarr; Object B(모니터 B)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server A에서 synchronized/ReentrantLock으로 임계영역을 막아도&lt;/li&gt;
&lt;li&gt;Server B에서는 &lt;span&gt;&lt;b&gt;완전히 별개의 락&lt;/b&gt;&lt;/span&gt;이라 동시에 들어올 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Oracle Docs : ReentrantLock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html&quot;&gt;https://docs.oracle.com/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771163586903&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Overview (Java SE 17 &amp;amp; JDK 17)&quot; data-og-description=&quot;This document is divided into two sections: Java SE The Java Platform, Standard Edition (Java SE) APIs define the core Java platform for general-purpose computing. These APIs are in modules whose names start with java. JDK The Java Development Kit (JDK) AP&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html&quot; data-og-url=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/index.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Overview (Java SE 17 &amp;amp; JDK 17)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This document is divided into two sections: Java SE The Java Platform, Standard Edition (Java SE) APIs define the core Java platform for general-purpose computing. These APIs are in modules whose names start with java. JDK The Java Development Kit (JDK) AP&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/521</guid>
      <comments>https://annovation.tistory.com/521#entry521comment</comments>
      <pubDate>Sun, 15 Feb 2026 22:50:57 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] JVM Lock : synchronized VS Reentrant Lock (실습중..)</title>
      <link>https://annovation.tistory.com/520</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;JVM 동시성 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;synchronized&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를 쓰면 &lt;/span&gt;&lt;b&gt;동시에 여러 스레드가 접근하는 상황(Thread interference)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 을 막는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 일관성 문제(memory consistency)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 도 예방한다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized는 모니터 락(intrinsic lock) 을 사용하는 동기화 메커니즘이다.&lt;/li&gt;
&lt;li&gt;한 객체에 대해 한 번에 한 스레드만 락을 획득할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Each object is associated with a monitor, which a thread can lock or unlock. Only one thread at a time may hold a lock on a monitor.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;synchronized 메서드/블록은 &lt;/span&gt;&lt;b&gt;이 모니터 락을 획득 &amp;rarr; 코드 실행 &amp;rarr; 자동 해제&lt;/b&gt;&lt;span&gt; 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;JVM은 락을 자동으로 획득/해제한다(프로그램이 직접 unlock 호출 X) &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Reentrant&amp;nbsp;Lock&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 동작은 &lt;span&gt;synchronized&lt;/span&gt;와 같다(상호 배제).&lt;/li&gt;
&lt;li&gt;&lt;span&gt;그러나 기능이 &lt;/span&gt;&lt;b&gt;더 많고 유연하다&lt;/b&gt;&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) &lt;b&gt;재진입성 (Reentrant)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 스레드가 같은 락을 여러 번 얻을 수 있다 &amp;rarr; &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;재진입 가능(Reentrant)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;A ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it&amp;hellip; The method will return immediately if the current thread already owns the lock.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771163388049&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lock.lock();
methodA();
lock.unlock();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ methodA 내부에서 또 lock.lock()을 호출 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) &lt;b&gt;공정성 설정 (fair)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;new ReentrantLock(true)&lt;span&gt; &amp;rarr; 오래 기다린 스레드 먼저!&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;반면 &lt;span&gt;synchronized&lt;/span&gt;는 기본적으로 공정성 보장을 제공하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) &lt;b&gt;Memory Sync&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;ReentrantLock&lt;/span&gt;&lt;span&gt; 역시 JVM의 &lt;/span&gt;&lt;span&gt;synchronized&lt;/span&gt;&lt;span&gt;와 동일한 &lt;/span&gt;&lt;b&gt;메모리 가시성 보장(happens-before)&lt;/b&gt;&lt;span&gt; 을 제공한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock &amp;hellip;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;비교&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;synchronized&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Reentrant&amp;nbsp;Lock&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 블로그 : [JAVA] synchronized VS Reentrant Lock (차이점)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@may_yun/JAVA-synchronized-VS-Reentrant-Lock-차이점&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@may_yun/JAVA-synchronized-VS-Reentrant-Lock-차이점&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771073309470&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[JAVA] synchronized VS Reentrant Lock (차이점)&quot; data-og-description=&quot;상호 배제를 통한 동기화의 개념을 선행하고 보면 이해하는데 도움이 됩니다.현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근 할 수 없도록 막는 개념Jav&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@may_yun/JAVA-synchronized-VS-Reentrant-Lock-차이점&quot; data-og-url=&quot;https://velog.io/@may_yun/JAVA-synchronized-VS-Reentrant-Lock-차이점&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bF5o2e/dJMb9fZrx5c/LS9NG2bDe6ERBW90615EX0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://velog.io/@may_yun/JAVA-synchronized-VS-Reentrant-Lock-차이점&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@may_yun/JAVA-synchronized-VS-Reentrant-Lock-차이점&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bF5o2e/dJMb9fZrx5c/LS9NG2bDe6ERBW90615EX0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[JAVA] synchronized VS Reentrant Lock (차이점)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;상호 배제를 통한 동기화의 개념을 선행하고 보면 이해하는데 도움이 됩니다.현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근 할 수 없도록 막는 개념Jav&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Oracle Docs : Synchronized&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771073688454&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Synchronized Methods (The Java&amp;trade; Tutorials &amp;gt;        
            Essential Java Classes &amp;gt; Concurrency)&quot; data-og-description=&quot;The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Dev.java for updated tutorials taking advantag&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Synchronized Methods (The Java&amp;trade; Tutorials &amp;gt; Essential Java Classes &amp;gt; Concurrency)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Dev.java for updated tutorials taking advantag&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) Oracle Docs : ReentrantLock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771074269165&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ReentrantLock (Java Platform SE 8 )&quot; data-og-description=&quot;Acquires the lock only if it is not held by another thread at the time of invocation. Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. Even when this lock has been set to&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ReentrantLock (Java Platform SE 8 )&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Acquires the lock only if it is not held by another thread at the time of invocation. Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. Even when this lock has been set to&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 블로그 : 처음부터 다시 배우는 Java 동시성 제어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/262&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://myvelop.tistory.com/262&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772025405909&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;처음부터 다시 배우는 Java 동시성 제어&quot; data-og-description=&quot;이 글에서는 synchronized 키워드의 JVM 모니터 락이 실제로 어떻게 동작하는지 살펴보고, ReentrantLock이 제공하는 동시성 처리 기능을 알아본 뒤, 동시성 프로그래밍을 할 때 발생할 수 있는 생산자-&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/262&quot; data-og-url=&quot;https://myvelop.tistory.com/262&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bC9s8O/dJMb9lL8F1y/QjTF1q2hPXuCkuDvEy3TzK/img.png?width=800&amp;amp;height=355&amp;amp;face=0_0_800_355,https://scrap.kakaocdn.net/dn/bCjmRO/dJMb9c9uZ2G/PTr7kqzjorYJHMpgibff00/img.png?width=800&amp;amp;height=355&amp;amp;face=0_0_800_355,https://scrap.kakaocdn.net/dn/cyhW1H/dJMb8WewJLL/EFvuAKVIfhKHhkQxDmSBJ1/img.png?width=1495&amp;amp;height=668&amp;amp;face=0_0_1495_668&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/262&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/262&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bC9s8O/dJMb9lL8F1y/QjTF1q2hPXuCkuDvEy3TzK/img.png?width=800&amp;amp;height=355&amp;amp;face=0_0_800_355,https://scrap.kakaocdn.net/dn/bCjmRO/dJMb9c9uZ2G/PTr7kqzjorYJHMpgibff00/img.png?width=800&amp;amp;height=355&amp;amp;face=0_0_800_355,https://scrap.kakaocdn.net/dn/cyhW1H/dJMb8WewJLL/EFvuAKVIfhKHhkQxDmSBJ1/img.png?width=1495&amp;amp;height=668&amp;amp;face=0_0_1495_668');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;처음부터 다시 배우는 Java 동시성 제어&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 synchronized 키워드의 JVM 모니터 락이 실제로 어떻게 동작하는지 살펴보고, ReentrantLock이 제공하는 동시성 처리 기능을 알아본 뒤, 동시성 프로그래밍을 할 때 발생할 수 있는 생산자-&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/520</guid>
      <comments>https://annovation.tistory.com/520#entry520comment</comments>
      <pubDate>Sat, 14 Feb 2026 22:10:56 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] Trouble Shooting : 테스트 코드 .env 환경 변수 적용</title>
      <link>https://annovation.tistory.com/518</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경 변수(DB_USERNAME)가 정상적으로 치환되지 않아 실제 DB 계정이 아닌 &lt;span&gt;${DB_USERNAME}&lt;/span&gt; 문자열로 접속을 시도하면서 인증에 실패한 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;background-color: #f6e199; color: #222222; text-align: start;&quot;&gt;문제 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;증상&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770947622961&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.hibernate.exception.GenericJDBCException: 
unable to obtain isolated JDBC connection 
[Access denied for user '${DB_USERNAME}'@'localhost' (using password: YES)]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : 루트에 위치한 .env 환경 변수 값을 읽어오지 못하는 상황&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048); color: #333333; text-align: start;&quot;&gt;본 프로젝트의 .env 파일은 루트 디렉터리(/Users/ann/Programming/Java Projects/hub-eleven/.env)에 위치하며, 루트는 settings.gradle 기준으로 정의한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048); color: #333333; text-align: start;&quot;&gt;해당 .env 파일의 DB_USERNAME 과 DB_PASSWORD 값을 읽어오지 못해 테스트용 로컬 MySQL 에 접속하지 못하는 에러 발생&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방안 검토&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 1 : .env 환경 변수란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경변수(Environment Variables)는 운영체제에서 제공하는 동적인 값으로, 애플리케이션이 실행되는 환경에 따라 설정 값을 다르게 적용할 수 있도록 도와준다. Spring Boot 프로젝트에서도 환경변수를 활용하면 코드 수정 없이 설정을 변경할 수 있어 유지보수성과 보안성이 향상된다. (&lt;a href=&quot;https://msj9965.tistory.com/5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 2 : 왜 문제가 되었나요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 방식별로 .env 주입 주체가 다르다 (IDE 실행 vs 테스트 실행)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;앱 실행(Run) &lt;/b&gt;&lt;/span&gt;: IntelliJ의 EnvFile 플러그인, Docker compose, 쉘 스크립트 등이 &lt;span&gt;.env&lt;/span&gt;를 읽어 환경변수로 넣어줌 (&lt;a href=&quot;https://www.jetbrains.com/help/idea/run-debug-configuration-spring-boot.html#more_options_before_launch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;테스트(Test) &lt;/b&gt;&lt;/span&gt;: Gradle/Maven 테스트 JVM은 별도 프로세스라서 위 주입이 적용되지 않음 (특히 IntelliJ에서 Run 구성과 Test 구성이 따로일 때)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 해결&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IntelliJ라면 &lt;span&gt;&lt;b&gt;Run/Debug Configurations에서 &amp;ldquo;테스트 구성&amp;rdquo;에도&lt;/b&gt;&lt;/span&gt; EnvFile(또는 Environment variables) 적용&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Gradle 사용 시 &lt;/span&gt;test { environment &quot;KEY&quot;,&quot;VALUE&quot; }&lt;span&gt;로 주입&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : &lt;/span&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;테스트 프로필 설정에&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;.env&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 명시적으로 import 하도록 설정&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #abb2bf;&quot;&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;import: &quot;optional:file:.env[.properties],optional:file:../.env[.properties]&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;file:.env : 현재 실행 디렉터리 기준&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;file:../.env&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 한 단계 상위(루트) 기준&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;[.properties]&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;.env&lt;/span&gt;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;KEY=VALUE&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형식으로 파싱&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: oklab(0.999994 0.0000455678 0.0000200868 / 0.048);&quot;&gt;optional:&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 파일이 없어도 앱 시작 실패 방지(디버깅 시 제거 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;그 외 더 다양한 해결 방법들도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://imdeepskyblue.tistory.com/66&quot;&gt;https://imdeepskyblue.tistory.com/66&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770950564772&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SpringBoot 애플리케이션에 .env 통합하는 방법 세가지&quot; data-og-description=&quot;안녕하세요. 저는 요즘에 합세랑 솦커톤, 과제로 인해서 프로젝트를 새로 세팅해야 하는 상황이 많았는데요!특히 환경 설정을 하면서 환경 변수를 분리하게 되는데 저는 분리하는 방법이 다양&quot; data-og-host=&quot;imdeepskyblue.tistory.com&quot; data-og-source-url=&quot;https://imdeepskyblue.tistory.com/66&quot; data-og-url=&quot;https://imdeepskyblue.tistory.com/66&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bquLRD/dJMb86OXV6O/Zl4ZeIYVqFbBRhQ4mfIpHK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/re1Ku/dJMb8Xkbvx8/ka6I1CPpLzYGN88nRsMiKK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225&quot;&gt;&lt;a href=&quot;https://imdeepskyblue.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://imdeepskyblue.tistory.com/66&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bquLRD/dJMb86OXV6O/Zl4ZeIYVqFbBRhQ4mfIpHK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/re1Ku/dJMb8Xkbvx8/ka6I1CPpLzYGN88nRsMiKK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SpringBoot 애플리케이션에 .env 통합하는 방법 세가지&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 저는 요즘에 합세랑 솦커톤, 과제로 인해서 프로젝트를 새로 세팅해야 하는 상황이 많았는데요!특히 환경 설정을 하면서 환경 변수를 분리하게 되는데 저는 분리하는 방법이 다양&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;imdeepskyblue.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;참고 자료&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 블로그 : [Spring Boot] 프로젝트에서 환경변수 설정하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://msj9965.tistory.com/5&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://msj9965.tistory.com/5&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770902415514&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Boot] 프로젝트에서 환경변수 설정하는 방법&quot; data-og-description=&quot;1. 환경변수란?환경변수(Environment Variables)는 운영체제에서 제공하는 동적인 값으로, 애플리케이션이 실행되는 환경에 따라 설정 값을 다르게 적용할 수 있도록 도와준다. Spring Boot 프로젝트에서&quot; data-og-host=&quot;msj9965.tistory.com&quot; data-og-source-url=&quot;https://msj9965.tistory.com/5&quot; data-og-url=&quot;https://msj9965.tistory.com/5&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cty7Wg/dJMb88eWIjT/Iy0zEk7JOgIA7m93RyDEhK/img.png?width=800&amp;amp;height=800&amp;amp;face=395_372_493_480,https://scrap.kakaocdn.net/dn/Z1QPN/dJMb84XUPR3/1ZJ5GwaqqwW1YMxex2jZ80/img.png?width=800&amp;amp;height=800&amp;amp;face=395_372_493_480,https://scrap.kakaocdn.net/dn/v4ZWi/dJMb84p4O6p/WXkBmq9wwpEQDZHEPMWrd0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=521_491_617_595&quot;&gt;&lt;a href=&quot;https://msj9965.tistory.com/5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://msj9965.tistory.com/5&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cty7Wg/dJMb88eWIjT/Iy0zEk7JOgIA7m93RyDEhK/img.png?width=800&amp;amp;height=800&amp;amp;face=395_372_493_480,https://scrap.kakaocdn.net/dn/Z1QPN/dJMb84XUPR3/1ZJ5GwaqqwW1YMxex2jZ80/img.png?width=800&amp;amp;height=800&amp;amp;face=395_372_493_480,https://scrap.kakaocdn.net/dn/v4ZWi/dJMb84p4O6p/WXkBmq9wwpEQDZHEPMWrd0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=521_491_617_595');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Boot] 프로젝트에서 환경변수 설정하는 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 환경변수란?환경변수(Environment Variables)는 운영체제에서 제공하는 동적인 값으로, 애플리케이션이 실행되는 환경에 따라 설정 값을 다르게 적용할 수 있도록 도와준다. Spring Boot 프로젝트에서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;msj9965.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>왜안되지?</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/518</guid>
      <comments>https://annovation.tistory.com/518#entry518comment</comments>
      <pubDate>Fri, 13 Feb 2026 12:48:12 +0900</pubDate>
    </item>
    <item>
      <title>[DBeaver] Access denied for user 'root@localhost' (using password : YES)</title>
      <link>https://annovation.tistory.com/517</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-10 at 4.45.48 PM.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmZlsb/dJMcacaYgSK/zHkMFzyfvE1s0FOMPAS3f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmZlsb/dJMcacaYgSK/zHkMFzyfvE1s0FOMPAS3f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmZlsb/dJMcacaYgSK/zHkMFzyfvE1s0FOMPAS3f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmZlsb%2FdJMcacaYgSK%2FzHkMFzyfvE1s0FOMPAS3f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2592&quot; height=&quot;1706&quot; data-filename=&quot;Screenshot 2026-02-10 at 4.45.48 PM.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;allowPublicKeyRetrieval = true 설정 이후, Access denied for user 'root@localhost' (using password : YES) 에러 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : 잘못된 Password 입력&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dmg 파일과 충돌로 인해 HomeBrew MySQL 을 재설치 했더니 Password 오류가 난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : Password 변경&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 실행중인 MySQL 종료&lt;/p&gt;
&lt;pre id=&quot;code_1770728329702&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql.server stop&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;brew services start mysql 로 서버를 켰다면 아래 명령어 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770728841272&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services stop mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Password 없이 MySQL에 접속하기 위해 아래 명령어 입력&lt;/p&gt;
&lt;pre id=&quot;code_1770728353131&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysqld_safe --skip-grant-tables&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) root 유저로 MySQL 접속&lt;/p&gt;
&lt;pre id=&quot;code_1770728368995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql -u root&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 터미널을 켜서 입력해야 들어가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) mysql DB 사용하도록 설정&lt;/p&gt;
&lt;pre id=&quot;code_1770728388920&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;use mysql;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) 권한 초기화&lt;/p&gt;
&lt;pre id=&quot;code_1770728418238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6) 비밀번호 변경&lt;/p&gt;
&lt;pre id=&quot;code_1770728433441&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER USER 'root'@'localhost' IDENTIFIED BY '새로운비밀번호';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;cf. 각 명령어는 반드시 세미콜론(;)으로 끝내야 하며, '새로운비밀번호' 부분에는 변경할 비밀번호를 입력하면 된다.&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7) 다시 권한 초기화&lt;/p&gt;
&lt;pre id=&quot;code_1770728447531&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8) &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;다음 exit 명령어를 통해 나갔다가 다시 접속하면 문제 없이 초기화 되어있는 것을 확인 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770728519770&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;exit;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9) 새 비밀번호로 접속 테스트&lt;/p&gt;
&lt;pre id=&quot;code_1770728538909&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql -u root -p&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10) 터미널 종료&lt;/p&gt;
&lt;pre id=&quot;code_1770729150566&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pkill mysqld
pkill mysqld_safe&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이걸 해줘야 'mysqld_safe --skip-grant-tables' 로 켜놨던 터미널이 종료된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11) MySQL 서버 재실행&lt;/p&gt;
&lt;pre id=&quot;code_1770729267712&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services start mysql&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;드디어&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;됐다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-10 at 10.17.53 PM.png&quot; data-origin-width=&quot;2680&quot; data-origin-height=&quot;1794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq7ywK/dJMcaaRLHEO/uSlezJkT4uPkC3ReYEVKG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq7ywK/dJMcaaRLHEO/uSlezJkT4uPkC3ReYEVKG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq7ywK/dJMcaaRLHEO/uSlezJkT4uPkC3ReYEVKG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq7ywK%2FdJMcaaRLHEO%2FuSlezJkT4uPkC3ReYEVKG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2680&quot; height=&quot;1794&quot; data-filename=&quot;Screenshot 2026-02-10 at 10.17.53 PM.png&quot; data-origin-width=&quot;2680&quot; data-origin-height=&quot;1794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;참고 자료&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 블로그 : 맥 환경에서 MySQL 비밀번호 재설정하기 (ver 9.0)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yermxx.tistory.com/70&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yermxx.tistory.com/70&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770728569122&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;맥 환경에서 MySQL 비밀번호 재설정하기 (ver 9.0)&quot; data-og-description=&quot;비밀번호 까먹으면 손이 고생하더라.. 비슷한 문제를 겪고 계신 분들에게 도움이 되길 바란다.&amp;nbsp;참고로 맥 환경 + MySQL 9.0 버전에서 시도했다.&amp;nbsp;&amp;nbsp;&amp;nbsp;MySQL 서버 재시작하기  &amp;nbsp;- 먼저 MySQL 서버를 중&quot; data-og-host=&quot;yermxx.tistory.com&quot; data-og-source-url=&quot;https://yermxx.tistory.com/70&quot; data-og-url=&quot;https://yermxx.tistory.com/70&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfNWWu/dJMb9lk23JD/7NMTnqx66wbu3buvhaacC0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cacd0E/dJMb88eWwqY/LBbK7K8gRDvi5trxvPoApk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://yermxx.tistory.com/70&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yermxx.tistory.com/70&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfNWWu/dJMb9lk23JD/7NMTnqx66wbu3buvhaacC0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cacd0E/dJMb88eWwqY/LBbK7K8gRDvi5trxvPoApk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;맥 환경에서 MySQL 비밀번호 재설정하기 (ver 9.0)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;비밀번호 까먹으면 손이 고생하더라.. 비슷한 문제를 겪고 계신 분들에게 도움이 되길 바란다.&amp;nbsp;참고로 맥 환경 + MySQL 9.0 버전에서 시도했다.&amp;nbsp;&amp;nbsp;&amp;nbsp;MySQL 서버 재시작하기  &amp;nbsp;- 먼저 MySQL 서버를 중&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yermxx.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 블로그 : Mac Mysql root계정 비밀번호 분실 해결하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev-woody.tistory.com/75&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev-woody.tistory.com/75&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770728590587&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Mac Mysql root계정 비밀번호 분실 해결하기&quot; data-og-description=&quot;brew unistall mysql을 통해서 Mysql을 지우고 다시 깔았지만, 비밀번호는 그대로 남아있어서 사용이 불가능했었다. 우선 실행중인 상태의 Mysql 서버를 종료시켜준다. mysql.server stop 다음 비밀번호 없이 &quot; data-og-host=&quot;dev-woody.tistory.com&quot; data-og-source-url=&quot;https://dev-woody.tistory.com/75&quot; data-og-url=&quot;https://dev-woody.tistory.com/75&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vC8fp/dJMb8U8Pwm3/Kl47P3vdSKSuuyNyTd0pzK/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/beMyXU/dJMb8PGr3lM/9xk5DDrwqyI6dQtkKDZR51/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/dMpEct/dJMb8RRNNPF/1UeNudYa5FU0jKjabuSPJK/img.png?width=3000&amp;amp;height=2000&amp;amp;face=0_0_3000_2000&quot;&gt;&lt;a href=&quot;https://dev-woody.tistory.com/75&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev-woody.tistory.com/75&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vC8fp/dJMb8U8Pwm3/Kl47P3vdSKSuuyNyTd0pzK/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/beMyXU/dJMb8PGr3lM/9xk5DDrwqyI6dQtkKDZR51/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/dMpEct/dJMb8RRNNPF/1UeNudYa5FU0jKjabuSPJK/img.png?width=3000&amp;amp;height=2000&amp;amp;face=0_0_3000_2000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Mac Mysql root계정 비밀번호 분실 해결하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;brew unistall mysql을 통해서 Mysql을 지우고 다시 깔았지만, 비밀번호는 그대로 남아있어서 사용이 불가능했었다. 우선 실행중인 상태의 Mysql 서버를 종료시켜준다. mysql.server stop 다음 비밀번호 없이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev-woody.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Tips ✨</category>
      <category>왜안되지?</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/517</guid>
      <comments>https://annovation.tistory.com/517#entry517comment</comments>
      <pubDate>Thu, 12 Feb 2026 09:48:28 +0900</pubDate>
    </item>
    <item>
      <title>[DBeaver] Public Key Retrieval is not allowed</title>
      <link>https://annovation.tistory.com/516</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-10 at 4.31.34 PM.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p2fNC/dJMcajnyBDS/cmx2o6C7bC7ifHv7KTJfs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p2fNC/dJMcajnyBDS/cmx2o6C7bC7ifHv7KTJfs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p2fNC/dJMcajnyBDS/cmx2o6C7bC7ifHv7KTJfs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp2fNC%2FdJMcajnyBDS%2Fcmx2o6C7bC7ifHv7KTJfs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2592&quot; height=&quot;1706&quot; data-filename=&quot;Screenshot 2026-02-10 at 4.31.34 PM.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DBeaver 에서 MySQL 연결 시도 중 Public Key Retrieval is not allowed 에러 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : MySQL 8.0 이상 보안 정책&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보안상의 이유로 MySQL 8.0 이상 부터 기본적으로 MySQL 서버는 클라이언트가 요청하는 공개키에 대해 허용하지 않도록 지원하기 떄문이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : Driver properties -&amp;gt; allowPublicKeyRetrieval -&amp;gt; true로 변경&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-10 at 4.33.15 PM.png&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brRKVv/dJMcah4oTgQ/XOdNOTRCkHPiMyrQ2Hw63K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brRKVv/dJMcah4oTgQ/XOdNOTRCkHPiMyrQ2Hw63K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brRKVv/dJMcah4oTgQ/XOdNOTRCkHPiMyrQ2Hw63K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrRKVv%2FdJMcah4oTgQ%2FXOdNOTRCkHPiMyrQ2Hw63K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2800&quot; height=&quot;1706&quot; data-filename=&quot;Screenshot 2026-02-10 at 4.33.15 PM.png&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;참고 자료&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 블로그 : DBeaver(디비버) Public Key Retrieval is not allowed 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://computer-science-student.tistory.com/719&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://computer-science-student.tistory.com/719&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770708751179&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;DBeaver(디비버) Public Key Retrieval is not allowed 에러&quot; data-og-description=&quot;DBeaver(디비버) Public Key Retrieval is not allowed 에러 디비버에서 MySQL 데이터베이스 접속을 잘하다가 갑자기 Public Key Retrieval is not allowed 에러를 만났다. MySQL의 8.x 버전 이후부터 발생하는 문제라고 한&quot; data-og-host=&quot;computer-science-student.tistory.com&quot; data-og-source-url=&quot;https://computer-science-student.tistory.com/719&quot; data-og-url=&quot;https://computer-science-student.tistory.com/719&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gUXTE/dJMb82MyUrE/EM9hkMxpysYpUV61O4QnVK/img.png?width=685&amp;amp;height=545&amp;amp;face=0_0_685_545,https://scrap.kakaocdn.net/dn/bmX4N3/dJMb9dHjT5n/TuxcVGD3jzRjoXiwbBRFJ1/img.png?width=685&amp;amp;height=545&amp;amp;face=0_0_685_545,https://scrap.kakaocdn.net/dn/e3tSY/dJMb9gxhfAs/PxZt3oQPuBq9JrEnqe2uWk/img.png?width=685&amp;amp;height=545&amp;amp;face=0_0_685_545&quot;&gt;&lt;a href=&quot;https://computer-science-student.tistory.com/719&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://computer-science-student.tistory.com/719&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gUXTE/dJMb82MyUrE/EM9hkMxpysYpUV61O4QnVK/img.png?width=685&amp;amp;height=545&amp;amp;face=0_0_685_545,https://scrap.kakaocdn.net/dn/bmX4N3/dJMb9dHjT5n/TuxcVGD3jzRjoXiwbBRFJ1/img.png?width=685&amp;amp;height=545&amp;amp;face=0_0_685_545,https://scrap.kakaocdn.net/dn/e3tSY/dJMb9gxhfAs/PxZt3oQPuBq9JrEnqe2uWk/img.png?width=685&amp;amp;height=545&amp;amp;face=0_0_685_545');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;DBeaver(디비버) Public Key Retrieval is not allowed 에러&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;DBeaver(디비버) Public Key Retrieval is not allowed 에러 디비버에서 MySQL 데이터베이스 접속을 잘하다가 갑자기 Public Key Retrieval is not allowed 에러를 만났다. MySQL의 8.x 버전 이후부터 발생하는 문제라고 한&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;computer-science-student.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 블로그 : Public Key Retrieval is not allowed 오류 해결하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wookku.tistory.com/272&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wookku.tistory.com/272&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770709157598&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[DBeaver] Public Key Retrieval is not allowed 오류 해결하기&quot; data-og-description=&quot;안녕하세요. 어느덧 여름이 지나고 이제는 제법 쌀쌀한 날씨가 되어가고 있습니다.낮에는 그럼에도 덥고, 아침과 저녁에는 추운 날씨네요. 저는 살짝 콧물을 훌쩍이며 글을 적고 있습니다.모두 &quot; data-og-host=&quot;wookku.tistory.com&quot; data-og-source-url=&quot;https://wookku.tistory.com/272&quot; data-og-url=&quot;https://wookku.tistory.com/272&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eJkCx/dJMb81fOuln/tDrXKefCcULEBKeNpOlVF1/img.png?width=800&amp;amp;height=315&amp;amp;face=0_0_800_315,https://scrap.kakaocdn.net/dn/cF9jP1/dJMb87f2fJO/9VAL2CU6LywgtPAI69Itf0/img.png?width=800&amp;amp;height=315&amp;amp;face=0_0_800_315,https://scrap.kakaocdn.net/dn/507pj/dJMb86OXGnZ/YdhG8kYE9bxSzSQ1FMeM11/img.png?width=886&amp;amp;height=349&amp;amp;face=0_0_886_349&quot;&gt;&lt;a href=&quot;https://wookku.tistory.com/272&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wookku.tistory.com/272&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eJkCx/dJMb81fOuln/tDrXKefCcULEBKeNpOlVF1/img.png?width=800&amp;amp;height=315&amp;amp;face=0_0_800_315,https://scrap.kakaocdn.net/dn/cF9jP1/dJMb87f2fJO/9VAL2CU6LywgtPAI69Itf0/img.png?width=800&amp;amp;height=315&amp;amp;face=0_0_800_315,https://scrap.kakaocdn.net/dn/507pj/dJMb86OXGnZ/YdhG8kYE9bxSzSQ1FMeM11/img.png?width=886&amp;amp;height=349&amp;amp;face=0_0_886_349');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[DBeaver] Public Key Retrieval is not allowed 오류 해결하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 어느덧 여름이 지나고 이제는 제법 쌀쌀한 날씨가 되어가고 있습니다.낮에는 그럼에도 덥고, 아침과 저녁에는 추운 날씨네요. 저는 살짝 콧물을 훌쩍이며 글을 적고 있습니다.모두&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wookku.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) MySQL Docs&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770709414891&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL :: MySQL Connector/J Developer Guide :: 6.3.5 Security&quot; data-og-description=&quot;MySQL Connector/J Developer Guide &amp;nbsp;/&amp;nbsp; ... &amp;nbsp;/&amp;nbsp; Connector/J Reference &amp;nbsp;/&amp;nbsp; Configuration Properties &amp;nbsp;/&amp;nbsp; Security paranoid Take measures to prevent exposure sensitive information in error messages and clear data structures holding sensitive data when p&quot; data-og-host=&quot;dev.mysql.com&quot; data-og-source-url=&quot;https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html&quot; data-og-url=&quot;https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL :: MySQL Connector/J Developer Guide :: 6.3.5 Security&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;MySQL Connector/J Developer Guide &amp;nbsp;/&amp;nbsp; ... &amp;nbsp;/&amp;nbsp; Connector/J Reference &amp;nbsp;/&amp;nbsp; Configuration Properties &amp;nbsp;/&amp;nbsp; Security paranoid Take measures to prevent exposure sensitive information in error messages and clear data structures holding sensitive data when p&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.mysql.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Tips ✨</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/516</guid>
      <comments>https://annovation.tistory.com/516#entry516comment</comments>
      <pubDate>Wed, 11 Feb 2026 09:00:52 +0900</pubDate>
    </item>
    <item>
      <title>[터미널 명령어] HomeBrew 명령어</title>
      <link>https://annovation.tistory.com/515</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1) brew 로 설치한 프로그램 리스트&lt;/p&gt;
&lt;pre id=&quot;code_1770706525374&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew ls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) MySQL 서버 켜져있는지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1770706724361&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services list | grep mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) MySQL 자동 실행 (재부팅 시 자동으로 다시 켜짐)&lt;/p&gt;
&lt;pre id=&quot;code_1770706550507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services start mysql&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Mac &lt;/span&gt;부팅할 때 자동으로 켜지고&lt;/li&gt;
&lt;li&gt;터미널을 닫아도 안 꺼지고&lt;/li&gt;
&lt;li&gt;로그아웃해도 계속 살아있고&lt;/li&gt;
&lt;li&gt;명시적으로 끄기 전까지 계속 실행되는 상태 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) MySQL 직접 끄기&lt;/p&gt;
&lt;pre id=&quot;code_1770706673495&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services stop mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) MySQL 지금만 켜기 (재부팅 시 다시 안켜짐)&lt;/p&gt;
&lt;pre id=&quot;code_1770706774731&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql.server start&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5-1) 서버 실행 중인지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1770706920983&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pgrep -fl mysqld&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️&lt;/p&gt;
&lt;pre id=&quot;code_1770707027912&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;3741 /.../opt/homebrew/.../mysqld_safe ...
3852 /opt/homebrew/.../mysqld ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5-2) 직접 끄기&lt;/p&gt;
&lt;pre id=&quot;code_1770707169973&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql.server stop&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Tips ✨</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/515</guid>
      <comments>https://annovation.tistory.com/515#entry515comment</comments>
      <pubDate>Tue, 10 Feb 2026 16:04:00 +0900</pubDate>
    </item>
    <item>
      <title>[MSA] Spring Cloud Config Client 동작 구조 (with Spring Docs)</title>
      <link>https://annovation.tistory.com/514</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;구조&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Config Client와 Config Server 연동&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Config Client는 Spring Boot 앱이 Config Server와 통신하는 역할&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;455&quot; data-start=&quot;365&quot;&gt;Spring Boot 애플리케이션이 Config Client를 포함하면&lt;br /&gt;&amp;rarr; Config Server에 HTTP로 접근해서 설정 데이터를 받아온다.&lt;/li&gt;
&lt;li data-end=&quot;537&quot; data-start=&quot;456&quot;&gt;받아온 설정 데이터는 &lt;b&gt;Spring Environment&lt;/b&gt;에 반영된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;ldquo;A Spring Boot application can take immediate advantage of the Spring Config Server (or other external property sources)&amp;hellip; and initializes Spring Environment with remote property sources. (&lt;a href=&quot;https://cloud.spring.io/spring-cloud-config/multi/multi__spring_cloud_config_client.html?utm_source&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 즉, Config Client는 &lt;b&gt;원격 중앙 설정 저장소(Config Server)&lt;/b&gt; 에서 설정을 가져와 Spring 애플리케이션의 기존 설정 환경에 &amp;ldquo;로딩&amp;rdquo;하는 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Config Server 설정 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Config Server 데이터를 가져오려면 아래와 같은 설정이 기본 형태&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770702658061&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.config.import=optional:configserver:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 이 설정이 있으면 Config Server를 기본적으로 http://localhost:8888로 연결 시도&lt;br /&gt;&amp;rarr; Optional prefix를 제거하면, 서버 연결 실패 시 바로 예외를 발생시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 레거시 방식인 &lt;b&gt;bootstrap&lt;/b&gt; 기반 연결도 지원&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 Config Server 주소를 bootstrap.yml에 설정하고 Config Client가 시작하면서 바로 서버와 연결된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;핵심 구조 요약&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1770702873432&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Spring Boot App
    │ (spring.cloud.config on classpath)
    ▼
Config Client 자동 구성
    │
    │ 1. spring.config.import 또는 bootstrap 설정으로 Config Server 위치 확인
    ▼
Config Server (HTTP endpoint, default localhost:8888)
    │
    │ 2. Remote config file(e.g., git repo)에서 설정 읽기
    ▼
Config Client
    │
    │ 3. Spring Environment에 remote properties merge
    ▼
ApplicationContext 완성&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-cloud-config/reference/client.html?utm_source&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770646123174&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Cloud Config Client :: Spring Cloud Config&quot; data-og-description=&quot;Retry works with the Spring Boot spring.config.import statement and the normal properties work. However, if the import statement is in a profile, such as application-prod.properties, then you need a different way to configure retry. Configuration needs to &quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html?utm_source=chatgpt.com&quot; data-og-url=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html?utm_source=chatgpt.com&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Config Client :: Spring Cloud Config&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Retry works with the Spring Boot spring.config.import statement and the normal properties work. However, if the import statement is in a profile, such as application-prod.properties, then you need a different way to configure retry. Configuration needs to&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>심화/MSA</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/514</guid>
      <comments>https://annovation.tistory.com/514#entry514comment</comments>
      <pubDate>Mon, 9 Feb 2026 23:22:47 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] MySQL 8.0.45-arm 64 켜졌다 꺼졌다 반복 on MacOs</title>
      <link>https://annovation.tistory.com/513</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맥 8.0.45-arm 64 환경에서 MySQL 서버가 켜졌다 꺼졌다를 반복하는 오류 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;background-color: #f6e199; color: #222222; text-align: start;&quot;&gt;문제 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;증상&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-07 at 10.38.17 PM.png&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eryWU5/dJMcac22EMB/EiVWuM3BKLmcUEwmCE7rpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eryWU5/dJMcac22EMB/EiVWuM3BKLmcUEwmCE7rpk/img.png&quot; data-alt=&quot;ON&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eryWU5/dJMcac22EMB/EiVWuM3BKLmcUEwmCE7rpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeryWU5%2FdJMcac22EMB%2FEiVWuM3BKLmcUEwmCE7rpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;1816&quot; data-filename=&quot;Screenshot 2026-02-07 at 10.38.17 PM.png&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1816&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ON&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-07 at 10.37.56 PM.png&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3a4c8/dJMcaiIWVWs/qX2nvXSxYx2LKKqWac38Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3a4c8/dJMcaiIWVWs/qX2nvXSxYx2LKKqWac38Yk/img.png&quot; data-alt=&quot;OFF&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3a4c8/dJMcaiIWVWs/qX2nvXSxYx2LKKqWac38Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3a4c8%2FdJMcaiIWVWs%2FqX2nvXSxYx2LKKqWac38Yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;1816&quot; data-filename=&quot;Screenshot 2026-02-07 at 10.37.56 PM.png&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1816&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OFF&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : HomeBrew 로 설치한 MySQL 과 Community Server 에서 설치한 dmg 버전 충돌&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) bash : HomeBrew 에 MySQL 설치되어있는지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1770471890952&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;which mysql
mysql --version
brew list | grep mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️&lt;/p&gt;
&lt;pre id=&quot;code_1770471925620&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/opt/homebrew/bin/mysql
mysql  Ver 9.0.1 for macos15.1 on arm64 (Homebrew)
mysql&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 클라이언트는 Homebrew 버전(9.0.1)을 쓰고 있다는 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) bash : MySQL 서버가 동작 중인지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1770471946793&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ps aux | grep mysqld&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 버전 2개&lt;/p&gt;
&lt;pre id=&quot;code_1770472929327&quot; class=&quot;dts&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/usr/local/mysql/bin/mysqld ... --datadir=/usr/local/mysql/data ...
/opt/homebrew/opt/mysql/bin/mysqld ... --datadir=/opt/homebrew/var/mysql ...&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공식 dmg(installer)로 설치된 MySQL 서버와 Homebrew로 설치된 MySQL 서버가 동시에 동작 중임을 알 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;분석 : 같은 포트(기본 3306)을 동시에 사용하려고 함&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금 어떤 MySQL 서버를 켰는지/어떤 클라이언트를 실행했는지 / 어떤 소켓&amp;middot;포트를 보고 있는지가 섞이기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방안 검토&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 1 : dmg 버전 제거, HomeBrew 만 사용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 우선 충돌 멈추기위해 dmg 버전과 HomeBrew 버전 정지&lt;/p&gt;
&lt;pre id=&quot;code_1770472730840&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;launchctl bootout gui/$(id -u)/homebrew.mxcl.mysql 2&amp;gt;/dev/null
launchctl disable gui/$(id -u)/homebrew.mxcl.mysql 2&amp;gt;/dev/null
pkill -9 mysqld_safe
pkill -9 mysqld&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;bootout&lt;/span&gt; &amp;rarr; launchd에서 MySQL 자동 실행 &lt;span&gt;&lt;b&gt;언로드&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;disable&lt;/span&gt;&lt;span&gt; &amp;rarr; 재부팅해도 &lt;/span&gt;&lt;b&gt;다시 안 올라오게 차단&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그 다음 &lt;span&gt;pkill&lt;/span&gt; &amp;rarr; 이제 감시자가 없으니 &lt;span&gt;&lt;b&gt;진짜로 종료&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 꺼졌다 켜졌다를 반복하기 때문에 PID (Process ID, 현재 실행중인 프로그램에 붙는 고유 번호) 를 알 수 없기 때문에 강제로 종료시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 제거 되었는지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1770611197444&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pkill -9 mysqld
pgrep -fl mysqld || echo &quot;mysqld 없음&quot;
mysqld 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) dmg MySQL 완전 제거&lt;/p&gt;
&lt;pre id=&quot;code_1770472075455&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo /usr/local/mysql/bin/mysql.server stop

sudo rm -rf /usr/local/mysql
sudo rm -rf /usr/local/mysql-*

sudo rm -rf /Library/StartupItems/MySQLCOM

sudo rm -rf /Library/PreferencePanes/MySQL.prefPane

sudo rm -rf /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist

sudo rm -rf /var/db/receipts/com.mysql.*&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 의미는 차례대로 :
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DMG(MySQL.pkg) 버전 서비스 중지&lt;/li&gt;
&lt;li&gt;DMG 버전 본체 디렉토리 삭제&lt;/li&gt;
&lt;li&gt;아주 옛날 DMG 버전에서 쓰던 &lt;span&gt;부팅 자동 실행 스크립트 삭제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;시스템 설정에 보이는 MySQL 설정 패널 제거&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Oracle DMG MySQL 자동 실행 launchd 데몬&lt;/li&gt;
&lt;li&gt;macOS의 설치 기록을 지우는 명령&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 비밀번호는 mac 로그인 password&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) dmg 버전 삭제되었는지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1770611636227&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ls -d /usr/local/mysql*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 아무것도 안나오면 삭제된 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770611653506&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo launchctl list | grep -i oracle || echo &quot;oracle mysql 데몬 없음&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) Homebrew 서비스로 실행&lt;/p&gt;
&lt;pre id=&quot;code_1770472150493&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services start mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6) 상태 확인&lt;/p&gt;
&lt;pre id=&quot;code_1770472167010&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services list
mysqladmin ping&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 실행이 안될 경우&lt;/p&gt;
&lt;pre id=&quot;code_1770612048747&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;error: 'Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러 메세지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770611981114&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ls -ld /opt/homebrew/var/mysql || mkdir -p /opt/homebrew/var/mysql&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 디렉토리 존재 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770612069934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/opt/homebrew/opt/mysql/bin/mysqld_safe \
  --datadir=/opt/homebrew/var/mysql \
  --socket=/tmp/mysql.sock &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;에러가 &lt;/span&gt;/tmp/mysql.sock&lt;span&gt; 없다고 했으니, 서버 실행 (socket을 /tmp/mysql.sock로 고정)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770612158066&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/opt/homebrew/opt/mysql/bin/mysqladmin --socket=/tmp/mysql.sock ping&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;살아있는지 확인 (socket 지정)&lt;/li&gt;
&lt;li&gt;mysqld is alive&lt;span&gt; 나오면 성공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770612201206&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/opt/homebrew/opt/mysql/bin/mysql --socket=/tmp/mysql.sock -u root&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접속&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770612217308&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tail -n 200 /opt/homebrew/var/mysql/*.err&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안켜지면 에러 로그 보기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : HomeBrew MySQL 재설치 ^_^&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://juzdalua.tistory.com/127&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://juzdalua.tistory.com/127&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770640355512&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;MySQL) M1 macbook MySQL 완전 삭제&quot; data-og-description=&quot;mysql을 설치했다 지웠는데 재설치 후 에러가 난다면, 기존에 삭제되지 않은 설정 값 때문일 수 있다. # mysql 강제종료 ps -ef | grep mysql kill -9 [mysql PID] # 삭제 brew services stop mysql brew uninstall mysql # 파&quot; data-og-host=&quot;juzdalua.tistory.com&quot; data-og-source-url=&quot;https://juzdalua.tistory.com/127&quot; data-og-url=&quot;https://juzdalua.tistory.com/127&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/F0n6v/dJMb89x9knr/Xu2VmFIf8WgKQxB4jyzYiK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bGCPhz/dJMb86OXBBB/708MgkZlo8CEmvZRiT1qI1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://juzdalua.tistory.com/127&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://juzdalua.tistory.com/127&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/F0n6v/dJMb89x9knr/Xu2VmFIf8WgKQxB4jyzYiK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bGCPhz/dJMb86OXBBB/708MgkZlo8CEmvZRiT1qI1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL) M1 macbook MySQL 완전 삭제&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;mysql을 설치했다 지웠는데 재설치 후 에러가 난다면, 기존에 삭제되지 않은 설정 값 때문일 수 있다. # mysql 강제종료 ps -ef | grep mysql kill -9 [mysql PID] # 삭제 brew services stop mysql brew uninstall mysql # 파&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;juzdalua.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Tips ✨</category>
      <category>왜안되지?</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/513</guid>
      <comments>https://annovation.tistory.com/513#entry513comment</comments>
      <pubDate>Sat, 7 Feb 2026 22:41:12 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] Trouble Shooting : 테스트 코드 Config Client 오류</title>
      <link>https://annovation.tistory.com/512</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 상황&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stock 테스트 코드에서 yml 설정으로 config server 를 false 설정 했음에도 관련 오류로 인해 테스트 실행이 안되는 상황 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제 코드&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- StockServiceTest.java&lt;/p&gt;
&lt;pre id=&quot;code_1770386299966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ActiveProfiles(&quot;test&quot;)
@SpringBootTest
class StockServiceImplTest {

    ...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- application-test.yml&lt;/p&gt;
&lt;pre id=&quot;code_1770386427380&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  cloud:
    config:
      enabled: false
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/hubeleven_test?useSSL=false&amp;amp;serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
    username: 비밀
    password: 비밀
  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
    show-sql: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199; color: #222222; text-align: start;&quot;&gt;문제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;증상&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770386751498&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ConfigClientFailFastException: Could not locate PropertySource and the resource is not optional
ResourceAccessException: I/O error on GET request for &quot;http://localhost:8888/...&quot;: Connection refused&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;원인 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 1 : Config Server 연결 시도&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 2.4+ / Spring Cloud&lt;span&gt; 조합에서 &lt;/span&gt;&lt;span&gt;spring.config.import=configserver:&lt;/span&gt;&lt;span&gt; 가 &lt;/span&gt;(application.yml 등 다른 파일에)&lt;span&gt; 있으면, &lt;/span&gt;application-local.yml이 다 로드되기 전에&lt;span&gt; configserver import를 먼저 처리하려고 시도한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;원인 2 : application-test.yml .env 환경 변수 읽어오기 실패&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770877754304&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/hubeleven_test?allowPublicKeyRetrieval=true&amp;amp;useSSL=false&amp;amp;serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 yml 에서 ${DB_USERNAME} 과 ${DB_PASSWORD} 을 불러오지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방안 검토&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 1 : main/resources/application.yml&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래의 test/resources/application-test.yml 에 config server 를 불러오지 않도록 설정된 값이 작동할 수 있게 main yml 파일을 수정해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770878351659&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  cloud:
    config:
      enabled: false&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 import 에 optional: 을 설정해줘야 test yml 에서 config server false 가 제대로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770878463380&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  application:
    name: product-service
  config:
    import: &quot;optional:configserver:&quot;
  cloud:
    config:
      discovery:
        enabled: true
        service-id: config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ optional: 은 Config Server 에서 설정을 못 가져와도 애플리케이션을 중단시키지 않고 계속 기동하게 만드는 방식 (&lt;a href=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ spring.config.import: 에 지정된 경로/서비스가 발견되면, Spring Boot 초기화 과정에서 추가적인 설정 데이터로 로딩되고 그 설정은 기존 설정보다 우선순위가 높게 적용된다. (&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/features/external-config.html?utm_source=chatgpt.com#features.external-config.files.importing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방안 2 : 일단 루트에 있는 .env 파일을 못읽어와서 테스트용 username 과 password 는 변수처리 하지 않고 명시적으로 작성&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770901669831&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  cloud:
    config:
      enabled: false
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/hubeleven_test?allowPublicKeyRetrieval=true&amp;amp;useSSL=false&amp;amp;serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
    username: test
    password: test&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 yml 파일에서 루트에 있는 .env 파일 읽어오는 방법을 따로 찾아봐야할 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;해결 방법&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;해결 방법 : 근데 결국 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;${DB_USERNAME},&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;${DB_PASSWORD} 얘네가 문제였다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음에 optional 도 붙여서 실행했는데도 테스트 코드 오류 났었는데, 이걸 해결하니까 다 잘 작동 된다..&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;span&gt;참고 자료&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Spring Docs&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-cloud-config/reference/client.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770898928815&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Cloud Config Client :: Spring Cloud Config&quot; data-og-description=&quot;Retry works with the Spring Boot spring.config.import statement and the normal properties work. However, if the import statement is in a profile, such as application-prod.properties, then you need a different way to configure retry. Configuration needs to &quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html&quot; data-og-url=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-cloud-config/reference/client.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Config Client :: Spring Cloud Config&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Retry works with the Spring Boot spring.config.import statement and the normal properties work. However, if the import statement is in a profile, such as application-prod.properties, then you need a different way to configure retry. Configuration needs to&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/512</guid>
      <comments>https://annovation.tistory.com/512#entry512comment</comments>
      <pubDate>Fri, 6 Feb 2026 22:55:29 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] Java 에서 발생하는 동시성 문제란?</title>
      <link>https://annovation.tistory.com/508</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Java 동시성 문제 (Race Condition, 레이스 컨디션)&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Java 동시성 문제란?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java에서 여러 스레드가 &lt;b&gt;같은 데이터(필드/객체 상태)&lt;/b&gt; 를 공유하며 실행될 때, 올바른 제어 없이 접근하면 오류가 생긴다.&lt;/li&gt;
&lt;li&gt;이렇게 여러 스레드가 공유 메모리(객체, 필드, 배열 등)에 접근할 때 명확한 동기화 규칙이 없어서, 값이 다르게 보이거나 실행 순서가 뒤틀리는 현상을 뜻한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;동시성 문제가 발생하는 이유&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Thread interference (스레드 간 간섭)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 스레드가 같은 데이터에 접근/수정하면서 연산이 서로 끼어들어 결과가 깨지는 문제 (전형적인 race condition의 형태)&lt;/li&gt;
&lt;li&gt;Java의 모든 객체의 필드, static 변수, 배열 요소는 힙 메모리에 존재하는데, 이 영역은 여러 스레드가 동시에 읽고, 쓸수 있기 때문에 접근 순서를 규정하지 않으면 문제가 발생한다. (&amp;sect;17.4.1 Shared Variables)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Memory consistency errors (메모리 일관성 오류)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 스레드가 쓴 값이 다른 스레드에서 &lt;b&gt;제때 보이지 않거나(stale)&lt;/b&gt;, 실행 순서가 기대와 다르게 관찰되는 문제(가시성/순서 문제)&lt;/li&gt;
&lt;li&gt;(&amp;sect;17.4 Memory Model, &amp;sect;17.4-A/B 예제)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;멀티스레드로 처리할 때 레이스 컨디션이 일어나는 이유&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;예상 작업 순서&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7674%;&quot;&gt;&lt;span&gt;&lt;b&gt;Thread-1&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;&lt;span&gt;&lt;b&gt;Stock&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.093%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;b&gt;Thread-2&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7674%;&quot;&gt;&lt;span&gt;select *from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;&lt;span&gt;{id: 1, quantity: 5}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.093%;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7674%;&quot;&gt;&lt;span&gt;update set quantity = 4from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;&lt;span&gt;{id: 1, quantity: 4}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.093%;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7674%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;{id: 1, quantity: 4}&lt;/td&gt;
&lt;td style=&quot;width: 42.093%;&quot;&gt;&lt;span&gt;select *from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7674%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;{id: 1, quantity: 3}&lt;/td&gt;
&lt;td style=&quot;width: 42.093%;&quot;&gt;&lt;span&gt;update set quantity = 3from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread-1 이 데이터를 가져가 update 한 값을 Thread-2 가 가져간 이후 update 하는 것을 예상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;실제 작업 순서&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7675%;&quot;&gt;&lt;span&gt;&lt;b&gt;Thread-1&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;&lt;span&gt;&lt;b&gt;Stock&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;&lt;span&gt;&lt;b&gt;Thread-2&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7675%;&quot;&gt;&lt;span&gt;select *from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;{id: 1, quantity: 5}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7675%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;&lt;span&gt;{id: 1, quantity: 5}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;&lt;span&gt;select *from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7675%;&quot;&gt;&lt;span&gt;update set quantity = 4from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;&lt;span&gt;{id: 1, quantity: 4}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.7675%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;&lt;span&gt;{id: 1, quantity: 4}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;&lt;span&gt;update set quantity = 4from stockwhere id = 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만, 실제로는 Thread-1 이 데이터를 가져가 update 하기 전에 Thread-2 가 값을 읽고 각각 update 를 수행하면서 한 쪽 변경이 덮어써지는 Lost Update 가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;출처&lt;/span&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Oracle Docs : Synchronization&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html?&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770210103499&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Synchronization (The Java&amp;trade; Tutorials &amp;gt;        
            Essential Java Classes &amp;gt; Concurrency)&quot; data-og-description=&quot;The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Dev.java for updated tutorials taking advantag&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html?&quot; data-og-url=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html?&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html?&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html?&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Synchronization (The Java&amp;trade; Tutorials &amp;gt; Essential Java Classes &amp;gt; Concurrency)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Dev.java for updated tutorials taking advantag&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Oracle Docs : Chapter 17. Threads and Locks&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770210568834&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Chapter&amp;nbsp;17.&amp;nbsp;Threads and Locks&quot; data-og-description=&quot;class A { final int x; A() { x = 1; } int f() { return d(this,this); } int d(A a1, A a2) { int i = a1.x; g(a1); int j = a2.x; return j - i; } static void g(A a) { // uses reflection to change a.x to 2 } } In the d method, the compiler is allowed to reorder&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Chapter&amp;nbsp;17.&amp;nbsp;Threads and Locks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;class A { final int x; A() { x = 1; } int f() { return d(this,this); } int d(A a1, A a2) { int i = a1.x; g(a1); int j = a2.x; return j - i; } static void g(A a) { // uses reflection to change a.x to 2 } } In the d method, the compiler is allowed to reorder&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 블로그 : 재고시스템으로 알아보는 동시성 이슈 해결방법 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@kdmin0706/Java-Spring-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%951&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@kdmin0706/Java-Spring&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770296707464&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java &amp;amp; Spring] 재고시스템으로 알아보는 동시성이슈 해결방법(1)&quot; data-og-description=&quot;인프런 &amp;quot;재고시스템으로 알아보는 동시성이슈 해결방법&amp;quot; 강의를 듣고 작성한 글입니다. 동시성 문제란!? 동일한 데이터에 2개 이상의 스레드 혹은, 세션에서 가변 데이터를 동시에 제어할 때 나&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@kdmin0706/Java-Spring-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%951&quot; data-og-url=&quot;https://velog.io/@kdmin0706/Java-Spring-재고시스템으로-알아보는-동시성이슈-해결방법1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zkePs/dJMb9frAUsM/2tacx5Fk85q4hJtI0y83Yk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/LDNvC/dJMb9iICzGj/CucCtKTBr1wKhjhKiCryL0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/UzJbE/dJMb9iaMAmi/Z3u5PVQkifpYyvaXdlFxKK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=337_258_764_1075&quot;&gt;&lt;a href=&quot;https://velog.io/@kdmin0706/Java-Spring-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%951&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@kdmin0706/Java-Spring-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%951&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zkePs/dJMb9frAUsM/2tacx5Fk85q4hJtI0y83Yk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/LDNvC/dJMb9iICzGj/CucCtKTBr1wKhjhKiCryL0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/UzJbE/dJMb9iaMAmi/Z3u5PVQkifpYyvaXdlFxKK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=337_258_764_1075');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Java &amp;amp; Spring] 재고시스템으로 알아보는 동시성이슈 해결방법(1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인프런 &quot;재고시스템으로 알아보는 동시성이슈 해결방법&quot; 강의를 듣고 작성한 글입니다. 동시성 문제란!? 동일한 데이터에 2개 이상의 스레드 혹은, 세션에서 가변 데이터를 동시에 제어할 때 나&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 블로그 : &lt;span style=&quot;color: #000000; letter-spacing: -1px; text-align: center; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;[재고시스템으로 알아보는 동시성 이슈 해결방법] #2 재고감소 로직 작성 및 테스트&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; letter-spacing: -1px; text-align: center; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&lt;a href=&quot;https://dev-rosiepoise.tistory.com/129&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev-rosiepoise.tistory.com/129&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770297546552&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[재고시스템으로 알아보는 동시성 이슈 해결방법] #2 재고감소 로직 작성 및 테스트&quot; data-og-description=&quot;들어가기 전 ..1. 이전 게시글과 이어지는 게시글이므로 프로젝트 환경 및 세팅은 아래 링크에서 확인해주세요!2. 전체 코드는 아래의 Git에서 확인 가능합니다!참고하면 좋을 이전 글&amp;nbsp;[재고시스&quot; data-og-host=&quot;dev-rosiepoise.tistory.com&quot; data-og-source-url=&quot;https://dev-rosiepoise.tistory.com/129&quot; data-og-url=&quot;https://dev-rosiepoise.tistory.com/129&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cX3UuM/dJMb9bvXKZ6/V0TjL5pICTFC4qXcvS7yhk/img.png?width=800&amp;amp;height=231&amp;amp;face=0_0_800_231,https://scrap.kakaocdn.net/dn/MaIIn/dJMb9gxgPgI/P35RMrGx639U3qMCqsvTGK/img.png?width=800&amp;amp;height=231&amp;amp;face=0_0_800_231,https://scrap.kakaocdn.net/dn/C4iod/dJMb8YpQRrE/qTlVXqW3HewyH9na9q6IbK/img.png?width=2643&amp;amp;height=649&amp;amp;face=0_0_2643_649&quot;&gt;&lt;a href=&quot;https://dev-rosiepoise.tistory.com/129&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev-rosiepoise.tistory.com/129&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cX3UuM/dJMb9bvXKZ6/V0TjL5pICTFC4qXcvS7yhk/img.png?width=800&amp;amp;height=231&amp;amp;face=0_0_800_231,https://scrap.kakaocdn.net/dn/MaIIn/dJMb9gxgPgI/P35RMrGx639U3qMCqsvTGK/img.png?width=800&amp;amp;height=231&amp;amp;face=0_0_800_231,https://scrap.kakaocdn.net/dn/C4iod/dJMb8YpQRrE/qTlVXqW3HewyH9na9q6IbK/img.png?width=2643&amp;amp;height=649&amp;amp;face=0_0_2643_649');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[재고시스템으로 알아보는 동시성 이슈 해결방법] #2 재고감소 로직 작성 및 테스트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가기 전 ..1. 이전 게시글과 이어지는 게시글이므로 프로젝트 환경 및 세팅은 아래 링크에서 확인해주세요!2. 전체 코드는 아래의 Git에서 확인 가능합니다!참고하면 좋을 이전 글&amp;nbsp;[재고시스&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev-rosiepoise.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/508</guid>
      <comments>https://annovation.tistory.com/508#entry508comment</comments>
      <pubDate>Wed, 4 Feb 2026 22:04:10 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 재고 감소 통합 테스트 코드 7 - 멀티 스레드</title>
      <link>https://annovation.tistory.com/507</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;멀티 스레드 테스트 코드&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;진행 환경&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Spring Boot 3.5.7&lt;/li&gt;
&lt;li&gt;MySQL 8.0.44&lt;/li&gt;
&lt;li&gt;Gradle&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;StockCurrencyTest.java&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770128784236&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
public class StockCurrencyTest {

    @Autowired
    private StockServiceImpl stockServiceImpl;

    @Autowired
    private StockRepository stockRepository;

    @Autowired
    private JpaStockRepository jpaStockRepository;

    @Autowired
    private JpaProductRepository jpaProductRepository;

    private Product product;

    @BeforeEach
    void setUp() {
        product = ProductFixture.createDefault();
        jpaProductRepository.saveAndFlush(product);

        Stock stock = StockFixture.createFromProductWithQuantity(product, 100);
        jpaStockRepository.saveAndFlush(stock);
    }

    @AfterEach
    void tearDown() {
        jpaStockRepository.deleteAll();
        jpaProductRepository.deleteAll();
    }

    @Test
    @DisplayName(&quot;재고 감소 - 100개 동시 요청 시 정확히 차감&quot;)
    void decreaseStock_when100ConcurrentRequests_thenSuccess() throws InterruptedException {

        // given
        int threadCount = 100;

        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		// when
        for (int i = 0; i &amp;lt; threadCount; i++) {
            executorService.submit(() -&amp;gt; {

                try {
                    stockServiceImpl.decreaseStock(
                            StockFixture.decreaseRequest(product, 1)
                    );
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();

		// then
        Stock updatedStock = stockRepository
                .findByProductId(product.getProductId())
                .orElseThrow();

        assertEquals(0, updatedStock.getQuantity());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고가 100개인 상품에 대해&amp;nbsp;100개의 스레드가 동시에 재고 1개씩 감소 요청&lt;span&gt; &amp;rarr; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;최종 재고가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;0이 되는지&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;검증&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;코드 흐름&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;given, when, then&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) given : 테스트 준비&lt;/p&gt;
&lt;pre id=&quot;code_1770266715433&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;threadCount = 100 &amp;rarr; 동시에 실행할 작업(요청) 수&lt;/li&gt;
&lt;li&gt;ExecutorService &amp;rarr; &lt;b&gt;100개의 스레드를 동시에 실행&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하기 위한 스레드 풀&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Java에서 멀티스레딩을 쉽게 관리하기 위한 인터페이스&lt;/li&gt;
&lt;li&gt;new Thread()를 여러 개 생성하는 대신 미리 만들어진 스레드 풀에서 스레드를 재사용&lt;/li&gt;
&lt;li&gt;execute(), submit() 등의 메서드를 사용해 비동기 작업 실행 가능 (&lt;a href=&quot;https://girokeulhaja.tistory.com/107&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CountDownLatch(100) &lt;span&gt;&amp;rarr; &lt;/span&gt;&lt;b&gt;모든 스레드가 작업을 끝낼 때까지 기다리기 위한 장치&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 스레드가 끝날 때마다 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;countDown()&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;latch.await()이 호출되면 요청이 모두 끝날 때까지 대기 후 다음 코드 실행 (&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://girokeulhaja.tistory.com/107&quot;&gt;출처&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;메인 스레드는 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;await()&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로 대기&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;2) when : 동시 요청 실행&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770266866783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; threadCount; i++) {
    executorService.submit(() -&amp;gt; {
        try {
            stockServiceImpl.decreaseStock(
                    StockFixture.decreaseRequest(product, 1)
            );
        } finally {
            countDownLatch.countDown();
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;100번 반복 &amp;rarr; &lt;/span&gt;&lt;b&gt;100개의 작업 제출&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;즉, &lt;b&gt;100명이 동시에 같은 상품 재고를 1개씩 감소시키는 상황&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 작업은 :
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;decreaseStock(product, 1)&lt;span&gt; 호출&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;같은 상품&lt;/li&gt;
&lt;li&gt;재고 1개 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;finally&lt;span&gt;에서 &lt;/span&gt;countDownLatch.countDown()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;예외가 나도 반드시 호출되게 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 모든 스레드 종료 대기&lt;/p&gt;
&lt;pre id=&quot;code_1770266970946&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;countDownLatch.await();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메인 테스트 스레드가 여기서 멈춤&lt;/li&gt;
&lt;li&gt;&lt;b&gt;100개의 재고 감소 요청이 모두 끝날 때까지 대기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이 줄이 없으면&amp;nbsp;&amp;rarr; 아직 작업이 끝나기 전에 결과를 조회해버릴 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) then : 결과 검증&lt;/p&gt;
&lt;pre id=&quot;code_1770267012345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stock updatedStock = jpaStockRepository
        .findByProductId(product.getProductId())
        .orElseThrow();

assertEquals(0, updatedStock.getQuantity());&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에서 최신 재고 조회&lt;/li&gt;
&lt;li&gt;최종 재고 수량이 &lt;span&gt;&lt;b&gt;0인지 확인&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/507</guid>
      <comments>https://annovation.tistory.com/507#entry507comment</comments>
      <pubDate>Tue, 3 Feb 2026 23:27:14 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 변수(Variables) 헷갈리는 문법 정리</title>
      <link>https://annovation.tistory.com/506</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;변수의 값&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수에 값을 대입하는 것은 변수에 들어있는 값을 복사해서 대입하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;기본형&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1)&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769955303789&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;int a = 10;
int b = a;

a = 20; // a 값 변경&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 b는 그대로 10이 나온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2)&lt;/p&gt;
&lt;pre id=&quot;code_1769955428908&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
	int a = 10;
	changePrimitive(a);

	static void changePrimitive(int x) {
				x = 20;
				
		}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;a = 10, x에 int 값 10이 복사되어 넘어가므로 x = 10&lt;/li&gt;
&lt;li&gt;changePrimitive에서 x = 20으로 바뀌므로 최종 결과는 a = 10, x = 20&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;참조형&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769955523408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Data dataA = new Data();
dataA.value = 10;
Data dataB = dataA;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dataA의 값을 바꾸나, dataB의 값을 바꾸나, 참조하고 있는 메모리 주소값이 같기 때문에 dataA.value와 dataB.value의 값은 언제나 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;변수 초기화&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;멤버 변수 : 자동 초기화&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화된다.&lt;/li&gt;
&lt;li&gt;int = 0, boolean = false, 참조형 = null&lt;/li&gt;
&lt;li&gt;개발자가 초기값을 직접 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;지역 변수 : 수동 초기화&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지역 변수는 항상 직접 초기화해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Java의 변수 타입 기본형(Primitive Type) vs 참조형 (Reference Type)&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;기본형 (Primitive Type)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;int, long, double, boolean 같이 변수에 직접 값을 넣을 수 있는 데이터 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;참조형 (Reference Type)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체와 배열같이 데이터에 접근하기 위해 참조 주소를 변수에 저장하는 데이터 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;형변환 (Type Casting)&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 타입끼리 계산은 값을 타입 반환 ex. int + int = int, double + double = double&lt;/li&gt;
&lt;li&gt;서로 다른 타입의 계산은 큰 범위로 자동 형변환 ex. int + double = double + double&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;출처&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%9E%90%EB%B0%94-%EC%9E%85%EB%AC%B8/dashboard?cid=332505&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%9E%90%EB%B0%94-%EC%9E%85%EB%AC%B8/dashboard?cid=332505&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769955025858&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[지금 무료]김영한의 자바 입문 - 코드로 시작하는 자바 첫걸음| 김영한 - 인프런 강의&quot; data-og-description=&quot;현재 평점 5.0점 수강생 54,098명인 강의를 만나보세요. 프로그래밍에 처음 입문하는 분들을 위한 자바 강의입니다. 코드를 따라하면서 손쉽게 자바를 배울 수 있습니다. 자바(Java) 프로그래밍 언&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%9E%90%EB%B0%94-%EC%9E%85%EB%AC%B8/dashboard?cid=332505&quot; data-og-url=&quot;https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%9E%90%EB%B0%94-%EC%9E%85%EB%AC%B8?cid=332505&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qCTlM/dJMb8Zvwri5/e3chmAALFOtjLiTTgpBSBk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cWDo5B/dJMb8XkarDr/1Ab9sTPwnya9XTiOfXLemk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/dc3MzP/dJMb8SpCXDL/f4NC0QpRSZljRj0bdvMjP1/img.jpg?width=1439&amp;amp;height=1441&amp;amp;face=171_677_281_798&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%9E%90%EB%B0%94-%EC%9E%85%EB%AC%B8/dashboard?cid=332505&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%9E%90%EB%B0%94-%EC%9E%85%EB%AC%B8/dashboard?cid=332505&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qCTlM/dJMb8Zvwri5/e3chmAALFOtjLiTTgpBSBk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cWDo5B/dJMb8XkarDr/1Ab9sTPwnya9XTiOfXLemk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/dc3MzP/dJMb8SpCXDL/f4NC0QpRSZljRj0bdvMjP1/img.jpg?width=1439&amp;amp;height=1441&amp;amp;face=171_677_281_798');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[지금 무료]김영한의 자바 입문 - 코드로 시작하는 자바 첫걸음| 김영한 - 인프런 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;현재 평점 5.0점 수강생 54,098명인 강의를 만나보세요. 프로그래밍에 처음 입문하는 분들을 위한 자바 강의입니다. 코드를 따라하면서 손쉽게 자바를 배울 수 있습니다. 자바(Java) 프로그래밍 언&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Java/[인프런] 자바 입문</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/506</guid>
      <comments>https://annovation.tistory.com/506#entry506comment</comments>
      <pubDate>Sun, 1 Feb 2026 23:24:29 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 재고 감소 통합 테스트 코드 6 - test DB</title>
      <link>https://annovation.tistory.com/505</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;테스트 코드를 위한 DB 연결&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;테스트 코드를 위한 DB 연결&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 보다 가벼운 H2 DB 를 사용해도 되지만, 실제 서비스 환경에서는 MySQL 을 사용하기 때문에 테스트의 신뢰성을 높이기 위해 MySQL DB 를 연결하기로 했다.&lt;/li&gt;
&lt;li&gt;Docker 로 테스트용 MySQL 을 팀원 공용으로 사용하면 DB 버전이나 설정 차이 없이 동일한 환경에서 테스트 가능하지만, 동시에 테스트를 진행하게될 경우 간섭을 받을 수 있어 우선 로컬에서 MySQL 을 생성하여 테스트하기로 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;테스트 코드를 위한 MySQL 생성&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Local MySQL 생성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) MySQL 과 DBeaver 연결&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설치 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://codingapple.com/unit/sql-mysql-dbeaver-install/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769848854422&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL &amp;amp; DBeaver 설치하려면 (맥 / 윈도우)&quot; data-og-description=&quot;1:08 윈도우에 MySQL 설치는 3:09 맥북에 MySQL 설치는 5:11 DBeaver 설치 &amp;amp; 데이터베이스 연결 DBMS가 뭐냐면 Database Management System 의 약자인데 데이터베이스 조작을 쉽게 도와주는 프로그램입니다.&amp;nbsp; DBMS를&quot; data-og-host=&quot;codingapple.com&quot; data-og-source-url=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; data-og-url=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cAWHVu/dJMb9lL6xiw/hp1EBOdjEoP0ZeHABwf5PK/img.png?width=971&amp;amp;height=690&amp;amp;face=0_0_971_690,https://scrap.kakaocdn.net/dn/bWvGf1/dJMb9lk17l9/BGzHlyklnDUW2stzbI27ck/img.png?width=970&amp;amp;height=691&amp;amp;face=0_0_970_691,https://scrap.kakaocdn.net/dn/cHj93X/dJMb9frAoJp/ww9h6K6VhK7CSnAb5DlLj0/img.png?width=967&amp;amp;height=692&amp;amp;face=0_0_967_692&quot;&gt;&lt;a href=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cAWHVu/dJMb9lL6xiw/hp1EBOdjEoP0ZeHABwf5PK/img.png?width=971&amp;amp;height=690&amp;amp;face=0_0_971_690,https://scrap.kakaocdn.net/dn/bWvGf1/dJMb9lk17l9/BGzHlyklnDUW2stzbI27ck/img.png?width=970&amp;amp;height=691&amp;amp;face=0_0_970_691,https://scrap.kakaocdn.net/dn/cHj93X/dJMb9frAoJp/ww9h6K6VhK7CSnAb5DlLj0/img.png?width=967&amp;amp;height=692&amp;amp;face=0_0_967_692');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL &amp;amp; DBeaver 설치하려면 (맥 / 윈도우)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1:08 윈도우에 MySQL 설치는 3:09 맥북에 MySQL 설치는 5:11 DBeaver 설치 &amp;amp; 데이터베이스 연결 DBMS가 뭐냐면 Database Management System 의 약자인데 데이터베이스 조작을 쉽게 도와주는 프로그램입니다.&amp;nbsp; DBMS를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codingapple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 데이터 베이스 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-01-31 at 5.52.32 PM.png&quot; data-origin-width=&quot;2447&quot; data-origin-height=&quot;1510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdAUmi/dJMcaajP1Uy/KfCte1Xt28I77rutx92a1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdAUmi/dJMcaajP1Uy/KfCte1Xt28I77rutx92a1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdAUmi/dJMcaajP1Uy/KfCte1Xt28I77rutx92a1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdAUmi%2FdJMcaajP1Uy%2FKfCte1Xt28I77rutx92a1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2447&quot; height=&quot;1510&quot; data-filename=&quot;Screenshot 2026-01-31 at 5.52.32 PM.png&quot; data-origin-width=&quot;2447&quot; data-origin-height=&quot;1510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 설치시 설정했던 Username 과 Password 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-01-31 at 5.57.55 PM.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xNMr2/dJMcahJ3kaz/q1Y27XuRs2Gzr6QumjOI2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xNMr2/dJMcahJ3kaz/q1Y27XuRs2Gzr6QumjOI2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xNMr2/dJMcahJ3kaz/q1Y27XuRs2Gzr6QumjOI2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxNMr2%2FdJMcahJ3kaz%2Fq1Y27XuRs2Gzr6QumjOI2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;1330&quot; data-filename=&quot;Screenshot 2026-01-31 at 5.57.55 PM.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 테스트용 MySQL DB 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① SQL문으로 직접 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770363640704&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE DATABASE IF NOT EXISTS hubEleven_test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② DBeaver 툴을 사용하여 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 1. 연결된 MySQL localhost 오른쪽 클릭 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;rarr;&lt;/span&gt; Create New Database&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.21.56 PM.png&quot; data-origin-width=&quot;2080&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/barkxl/dJMcaia9SXi/xR4gWtIeKfMr003xn0736k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/barkxl/dJMcaia9SXi/xR4gWtIeKfMr003xn0736k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/barkxl/dJMcaia9SXi/xR4gWtIeKfMr003xn0736k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbarkxl%2FdJMcaia9SXi%2FxR4gWtIeKfMr003xn0736k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2080&quot; height=&quot;1374&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.21.56 PM.png&quot; data-origin-width=&quot;2080&quot; data-origin-height=&quot;1374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 2. Database 이름 설정 후 OK&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.28.48 PM.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceNQjx/dJMcafZLV12/7jxph2iOYPe0KRWx4i01Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceNQjx/dJMcafZLV12/7jxph2iOYPe0KRWx4i01Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceNQjx/dJMcafZLV12/7jxph2iOYPe0KRWx4i01Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceNQjx%2FdJMcafZLV12%2F7jxph2iOYPe0KRWx4i01Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2592&quot; height=&quot;1706&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.28.48 PM.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 새로 생성한 로컬 MySQL 프로젝트 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;yml 설정&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770365257932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  cloud:
    config:
      enabled: false
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/hubEleven_test?useSSL=false&amp;amp;serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
    show-sql: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) &lt;b&gt;spring.cloud.config.enabled: false&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운영 환경에서는 Config Server를 사용하지만, 로컬 테스트 환경에서는 &lt;b&gt;&lt;/b&gt;외부 인프라 의존성을 제거하기 위해 Config Client를 비활성화&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) &lt;b&gt;spring.datasource.*&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770365407687&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hubEleven_test
username: ${DB_USERNAME}
password: ${DB_PASSWORD}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 테스트 전용 DB 사용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;hubEleven_test&lt;/span&gt;&lt;span&gt;를 사용해서 &lt;/span&gt;&lt;b&gt;운영 DB와 물리적으로 분리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실수로 데이터 삭제/변경되는 사고 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 환경변수로 계정 정보 관리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계정/비밀번호를 yml에 직접 쓰지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안 + 환경별 설정 분리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;프로젝트 연결&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) intelliJ 오른쪽 DB 메뉴에서 MySQL DB 연결&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.37.22 PM.png&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byBTxq/dJMcagEpYJS/XyIENrK10aULkwTukapYLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byBTxq/dJMcagEpYJS/XyIENrK10aULkwTukapYLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byBTxq/dJMcagEpYJS/XyIENrK10aULkwTukapYLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyBTxq%2FdJMcagEpYJS%2FXyIENrK10aULkwTukapYLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;551&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.37.22 PM.png&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) DBeaver 에서 생성한 test DB 연결해주면 끄읏!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.38.41 PM.png&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM95Rx/dJMcabC59fZ/GkKPbbzF8vbMOV0JxhOJNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM95Rx/dJMcabC59fZ/GkKPbbzF8vbMOV0JxhOJNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM95Rx/dJMcabC59fZ/GkKPbbzF8vbMOV0JxhOJNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM95Rx%2FdJMcabC59fZ%2FGkKPbbzF8vbMOV0JxhOJNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1824&quot; height=&quot;1584&quot; data-filename=&quot;Screenshot 2026-02-06 at 5.38.41 PM.png&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;개선되어야 할 점&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 Docker로 띄우는 MySQL 포트 localhost:3306 과 테스트용 로컬 MySQL &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;localhost:3306&lt;span&gt; 가 같기 때문에 두 서버를 동시에 띄울 수 없다는 한계가 존재한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;이 경우 Docker MySQL 포트를 다른 서버에 포워딩하는 방식이 해결 방법 중 하나가 될 수 있을 것 같다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770364659873&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run --name mark-board-mysql -e MYSQL_ROOT_PASSWORD=root -p 3307:3306 -d mysql:8.0.38&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;참고 자료&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1) 코딩 애플 : MySQL &amp;amp; DBeaver 설치하려면 (맥 / 윈도우)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://codingapple.com/unit/sql-mysql-dbeaver-install/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770363343546&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL &amp;amp; DBeaver 설치하려면 (맥 / 윈도우)&quot; data-og-description=&quot;1:08 윈도우에 MySQL 설치는 3:09 맥북에 MySQL 설치는 5:11 DBeaver 설치 &amp;amp; 데이터베이스 연결 DBMS가 뭐냐면 Database Management System 의 약자인데 데이터베이스 조작을 쉽게 도와주는 프로그램입니다.&amp;nbsp; DBMS를&quot; data-og-host=&quot;codingapple.com&quot; data-og-source-url=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; data-og-url=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RA6F1/dJMb9lk2GlV/mQWk0HVGncugqbQMMdMakK/img.png?width=971&amp;amp;height=690&amp;amp;face=0_0_971_690,https://scrap.kakaocdn.net/dn/2ugTE/dJMb9bvXPku/6G8OlOu3sQIkEK2YJhkrD1/img.png?width=970&amp;amp;height=691&amp;amp;face=0_0_970_691,https://scrap.kakaocdn.net/dn/hDzDR/dJMb9jOivar/R906MdCIOU8yyFnFO0Pu31/img.png?width=967&amp;amp;height=692&amp;amp;face=0_0_967_692&quot;&gt;&lt;a href=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codingapple.com/unit/sql-mysql-dbeaver-install/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RA6F1/dJMb9lk2GlV/mQWk0HVGncugqbQMMdMakK/img.png?width=971&amp;amp;height=690&amp;amp;face=0_0_971_690,https://scrap.kakaocdn.net/dn/2ugTE/dJMb9bvXPku/6G8OlOu3sQIkEK2YJhkrD1/img.png?width=970&amp;amp;height=691&amp;amp;face=0_0_970_691,https://scrap.kakaocdn.net/dn/hDzDR/dJMb9jOivar/R906MdCIOU8yyFnFO0Pu31/img.png?width=967&amp;amp;height=692&amp;amp;face=0_0_967_692');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL &amp;amp; DBeaver 설치하려면 (맥 / 윈도우)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1:08 윈도우에 MySQL 설치는 3:09 맥북에 MySQL 설치는 5:11 DBeaver 설치 &amp;amp; 데이터베이스 연결 DBMS가 뭐냐면 Database Management System 의 약자인데 데이터베이스 조작을 쉽게 도와주는 프로그램입니다.&amp;nbsp; DBMS를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codingapple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Docker MySQL 포트 충돌 문제 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hwangjoonyoung.com/posts/docker-mysql-port/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hwangjoonyoung.com/posts/docker-mysql-port/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770364698736&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Docker MySQL 포트 충돌 문제 해결 - Blog&quot; data-og-description=&quot;도커 환경을 구성하면서 마주친 문제&quot; data-og-host=&quot;hwangjoonyoung.com&quot; data-og-source-url=&quot;https://hwangjoonyoung.com/posts/docker-mysql-port/&quot; data-og-url=&quot;https://hwangjoonyoung.com/posts/docker-mysql-port/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/i6aPS/dJMb8Z3mMku/GqOomYjIoZkFn835Gj3jW1/img.png?width=1381&amp;amp;height=206&amp;amp;face=0_0_1381_206,https://scrap.kakaocdn.net/dn/c2lHl2/dJMb8XkaU4A/9GoxeOsHXAPhAVoLUVZ7A0/img.png?width=1023&amp;amp;height=162&amp;amp;face=0_0_1023_162,https://scrap.kakaocdn.net/dn/gWbSm/dJMb8SXtksw/pEsaFRbSvruU6WZAp17Fa0/img.png?width=1274&amp;amp;height=718&amp;amp;face=0_0_1274_718&quot;&gt;&lt;a href=&quot;https://hwangjoonyoung.com/posts/docker-mysql-port/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hwangjoonyoung.com/posts/docker-mysql-port/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/i6aPS/dJMb8Z3mMku/GqOomYjIoZkFn835Gj3jW1/img.png?width=1381&amp;amp;height=206&amp;amp;face=0_0_1381_206,https://scrap.kakaocdn.net/dn/c2lHl2/dJMb8XkaU4A/9GoxeOsHXAPhAVoLUVZ7A0/img.png?width=1023&amp;amp;height=162&amp;amp;face=0_0_1023_162,https://scrap.kakaocdn.net/dn/gWbSm/dJMb8SXtksw/pEsaFRbSvruU6WZAp17Fa0/img.png?width=1274&amp;amp;height=718&amp;amp;face=0_0_1274_718');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Docker MySQL 포트 충돌 문제 해결 - Blog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;도커 환경을 구성하면서 마주친 문제&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hwangjoonyoung.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/505</guid>
      <comments>https://annovation.tistory.com/505#entry505comment</comments>
      <pubDate>Sat, 31 Jan 2026 17:10:53 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 재고 감소 통합 테스트 코드 5 - 단일 스레드</title>
      <link>https://annovation.tistory.com/504</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;단일 스레드 환경 리팩토링 with TestFixture&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Test Fixture 도입 전 단일 스레드 테스트 코드&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) @BeforeEach&lt;/p&gt;
&lt;pre id=&quot;code_1769761543411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeEach
    public void setUp() {

        // 테스트용 고정 ID 생성 (각 테스트마다 새로 생성)
        testProductId = UUID.randomUUID(); // TODO : Test Fixture
        testCompanyId = UUID.randomUUID();
        testHubId = UUID.randomUUID();

        Stock stock = Stock.create(testProductId, testCompanyId, testHubId, 100);
        jpaStockRepository.saveAndFlush(stock);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) decreaseStock_success&lt;/p&gt;
&lt;pre id=&quot;code_1769761588909&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    @DisplayName(&quot;재고 감소 - 단일 요청 성공&quot;)
    void decreaseStock_success() {

        // given
        int decreaseAmount = 10;

        StockRequests.Decrease request = new StockRequests.Decrease(testProductId, decreaseAmount);

        // when
        StockResult result = stockServiceImpl.decreaseStock(request);

        // then - 반환값 검증
        assertThat(result.productId()).isEqualTo(testProductId);
        assertThat(result.productId()).isEqualTo(testCompanyId);
        assertThat(result.productId()).isEqualTo(testHubId);
        assertThat(result.quantity()).isEqualTo(90L);

        // then - 실제 DB 재고 검증
        Stock updatedStock =
                jpaStockRepository
                        .findByProductId(testProductId)
                        .orElseThrow(() -&amp;gt; new AssertionError(&quot;재고가 존재해야 합니다&quot;));

        assertThat(updatedStock.getQuantity()).isEqualTo(90L);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Test Fixture 도입 후 단일 스레드 테스트 코드&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) @BeforeEach&lt;/p&gt;
&lt;pre id=&quot;code_1769762385335&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeEach
    public void setUp() {

        product = ProductFixture.createDefault();
        jpaProductRepository.saveAndFlush(product);

        Stock stock = StockFixture.createFromProductWithQuantity(product, 100);
        jpaStockRepository.saveAndFlush(stock);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fixture 로 책임 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) decreaseStock_success&lt;/p&gt;
&lt;pre id=&quot;code_1769762448767&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    @DisplayName(&quot;재고 감소 - 단일 요청 성공&quot;)
    void decreaseStock_success() {

        // given
        int decreaseAmount = 10;

        StockRequests.Decrease request = StockFixture.decreaseRequest(
                product, decreaseAmount);

        // when
        StockResult result = stockServiceImpl.decreaseStock(request);

        // then - 반환값 검증
        assertThat(result.productId()).isEqualTo(product.getProductId());
        assertThat(result.companyId()).isEqualTo(product.getCompanyId());
        assertThat(result.hubId()).isEqualTo(product.getHubId());
        assertThat(result.quantity()).isEqualTo(90);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;ProductFixture.java&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;test/java/com.hubeleven.product/stock/application/fixtures/ProductFixture.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770207794796&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductFixture {

    // ===== ID =====

    public static final UUID COMPANY_ID = UUID.randomUUID();

    public static final UUID HUB_ID = UUID.randomUUID();

    // ===== Factory Methods =====

    public static Product createDefault() {
        return Product.create(
                &quot;Default Product&quot;,
                COMPANY_ID,
                HUB_ID
        );
    }
    
    private ProductFixture() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stock 이 생성되려면 Product 가 먼저 만들어져야하기 때문에 ProductFixture 도 만들어줘야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;StockFixture.java&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199; color: #333333; text-align: start;&quot;&gt;test/java/com.hubeleven.product/stock/application/fixtures/StockFixture.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769762575129&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class StockFixture {

    // ===== Factory Methods =====

    public static Stock createFromProductWithQuantity(Product product, int quantity) {
        return Stock.create(
                product.getProductId(),
                product.getCompanyId(),
                product.getHubId(),
                quantity
        );
    }

    public static StockRequests.Decrease decreaseRequest(Product product, int quantity) {
        return new StockRequests.Decrease(
                product.getProductId(),
                quantity
        );
    }

    private StockFixture() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우선 현재 테스트에서 바뀌는 필드는 quantity 하나이기 때문에, Builder 패턴은 도입하지 않았다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/504</guid>
      <comments>https://annovation.tistory.com/504#entry504comment</comments>
      <pubDate>Fri, 30 Jan 2026 17:27:11 +0900</pubDate>
    </item>
    <item>
      <title>[동시성 처리] 재고 감소 통합 테스트 코드 4 - 단일 스레드</title>
      <link>https://annovation.tistory.com/503</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;border-bottom: 12px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;단일 스레드 환경 with Test Fixture&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;test/java/com.hubeleven.product/stock/application/fixtures/StockFixture.java&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stock 도메인 테스트용 StockFixture 클래스 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1769698283363&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// fixtures/StockFixture.java
public class StockFixture {

    // ===== Factory Methods =====

    public static Stock createDefault() {
        return Stock.create(PRODUCT_ID, COMPANY_ID, HUB_ID, 100);
    }

    public static Stock createWithQuantity(int quantity) {
        return Stock.create(PRODUCT_ID, COMPANY_ID, HUB_ID, quantity);
    }

    public static Stock lowStock() {
        return createWithQuantity(5);
    }

    public static Stock outOfStock() {
        return createWithQuantity(0);
    }

    // ===== Builder =====

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private UUID productId = PRODUCT_ID;
        private UUID companyId = COMPANY_ID;
        private UUID hubId = HUB_ID;
        private int quantity = 100;

        public Builder productId(UUID productId) {
            this.productId = productId;
            return this;
        }

        public Builder companyId(UUID companyId) {
            this.companyId = companyId;
            return this;
        }

        public Builder hubId(UUID hubId) {
            this.hubId = hubId;
            return this;
        }

        public Builder quantity(int quantity) {
            this.quantity = quantity;
            return this;
        }

        public Stock build() {
            return Stock.create(productId, companyId, hubId, quantity);
        }
    }

    private StockFixture() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Factory Methods&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769699493856&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ===== Factory Methods =====

    public static Stock createDefault() {
        return Stock.create(PRODUCT_ID, COMPANY_ID, HUB_ID, 100);
    }

    public static Stock createWithQuantity(int quantity) {
        return Stock.create(PRODUCT_ID, COMPANY_ID, HUB_ID, quantity);
    }

    public static Stock lowStock() {
        return createWithQuantity(5);
    }

    public static Stock outOfStock() {
        return createWithQuantity(0);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 테스트가 무엇을 검증하는지 명확히 드러나는 Factory 메서드 구현&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Builder&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769699524199&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ===== Builder =====

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private UUID productId = PRODUCT_ID;
        private UUID companyId = COMPANY_ID;
        private UUID hubId = HUB_ID;
        private int quantity = 100;

        public Builder productId(UUID productId) {
            this.productId = productId;
            return this;
        }

        public Builder companyId(UUID companyId) {
            this.companyId = companyId;
            return this;
        }

        public Builder hubId(UUID hubId) {
            this.hubId = hubId;
            return this;
        }

        public Builder quantity(int quantity) {
            this.quantity = quantity;
            return this;
        }

        public Stock build() {
            return Stock.create(productId, companyId, hubId, quantity);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 필요한 값으로 바꿔서 사용 가능한 Builder 패턴 구현&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;인스턴스 생성 차단&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769699602480&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private StockFixture() {}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StockFixture 클래스는 객체를 생성하기 위한 클래스가 아니기 때문에 new StockFixture() 는 의미가 없다.&lt;/li&gt;
&lt;li&gt;Java 에서는 생성자를 만들지 않으면 기본 생성자가 자동으로 만들어지지만, StockFixutre 는 인스턴스가 아무 역할도 하지 않기 때문에 쓸모 없는 객체 생성으로 인해 잘못된 사용 가능성이 있다.&lt;/li&gt;
&lt;li&gt;이를 위해 private 으로 인스턴스 생성을 차단한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Projects/hub-eleven</category>
      <category>til</category>
      <author>annovation</author>
      <guid isPermaLink="true">https://annovation.tistory.com/503</guid>
      <comments>https://annovation.tistory.com/503#entry503comment</comments>
      <pubDate>Thu, 29 Jan 2026 23:51:28 +0900</pubDate>
    </item>
  </channel>
</rss>