<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>코드레이서</title>
        <link>https://velog.io/</link>
        <description>🏎️💨 Beep Beep</description>
        <lastBuildDate>Tue, 19 Mar 2024 14:20:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>코드레이서</title>
            <url>https://velog.velcdn.com/images/dev_lee/profile/c553479c-687a-4897-b7af-d12e34deddab/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 코드레이서. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_lee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[🏎️ [성능개선] N페이 승인 대사 속도 개선]]></title>
            <link>https://velog.io/@dev_lee/%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-N%ED%8E%98%EC%9D%B4-%EC%8A%B9%EC%9D%B8-%EB%8C%80%EC%82%AC-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@dev_lee/%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-N%ED%8E%98%EC%9D%B4-%EC%8A%B9%EC%9D%B8-%EB%8C%80%EC%82%AC-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Tue, 19 Mar 2024 14:20:03 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>승인 대사란 PG사의 승인 및 취소된 데이터와 당사에서 관리되는 데이터의 일치 여부를 확인하는 작업이에요. </p>
<blockquote>
<p>PG란 Payment Gateway의 약자로 온라인 결제 대행사를 뜻해요.</p>
</blockquote>
<h2 id="승인대사의-중요성">승인대사의 중요성</h2>
<ol>
<li><strong>무결성 확보</strong>: 결제 승인대사는 고객이 결제한 금액과 실제로 승인된 금액이 일치하는지 확인함으로써 데이터의 무결성을 보장해요.</li>
<li><strong>오류 발견 및 수정</strong>: 결제 과정에서 발생할 수 있는 오류나 불일치를 조기에 발견하여 수정할 수 있어요. 예를 들어, 고객이 결제한 금액과 PG사에서 승인된 금액이 다를 경우, 이를 신속하게 찾아내어 적절한 조치를 취할 수 있어요.</li>
<li><strong>재무 정보 신뢰성 향상</strong>: 승인대사를 통해 확보된 정확한 데이터는 기업의 재무 정보가 신뢰할 수 있는 상태로 유지되도록 해요. 이는 경영진이 올바른 의사 결정을 내리는 데 도움을 주며, 외부 감사나 규제 기관의 요구에도 적절히 대응할 수 있어요.</li>
</ol>
<p>이번 승인 대사 개선 건은 네이버 페이 결제 수단의 승인 대사에서 유독 속도가 지연되고 있어 개선하고자 했어요.</p>
<h2 id="as-is">AS-IS</h2>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/c488fef3-e712-4273-b39e-9d1ab2045cf6/image.png" alt=""></p>
<p>기존의 경우 승인 대사 배치 속도는 최대 23390ms, 최소 29ms, 평균 3442.143ms를 기록하고 있어요. 위의 통계 데이터는 이번에 개선하고자한 승인 대사 뿐만 아니라 다양한 결제 수단에 대한 통계 수치에요. 저희 서비스에서는 하나의 API로 다양한 결제 수단 승인 대사가 배치로 이루어지고 있어요. 하나의 API로 이루어지고 있지만, 저희 시스템에서는 L사 모니터링 솔루션을 통해 특정 승인대사의 모니터링이 가능해요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/09f4bae6-3ad9-4808-bd55-6936d4a251d6/image.png" alt=""></p>
<p>여기서 24초에 근접한 포인트가 바로 이번에 개선하려는 결제 수단이에요. 다른 결제 수단의 승인 대사에 비해 월등히 높은 수치를 기록하고 있어요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/9d5c4b91-302b-4f6f-bc89-f0b0d729e86c/image.png" alt=""></p>
<p>해당 결제 수단의 승인 대사는 벌크(bulk)로 대량의 데이터를 가져와 처리하는 것이 아니라 페이지네이션 API를 통한 데이터를 수집하여 <strong><em>반복문을 통해 API를 호출</em></strong>하고 있어요(우측 하단 부분의 빨간 네모 상자들이 API 호출을 의미해요). 이로 인해 수 천 건 이상의 데이터를 한번에 가져오는 것이 아닌, 수 천 건에서 <strong><em>페이지네이션된 개수만큼 한 페이지, 한 페이지 호출하여 데이터를 수집</em></strong>해 지연(delay) 되는 시간이 길어 발생된 이유였어요.</p>
<h2 id="to-be">TO-BE</h2>
<p>우선 해당 결제 수단의 승인 대사 API가 벌크 API를 제공하는지 여부를 확인했어요. 벌크 API를 제공한다면 페이지마다 반복문을 통해 API를 호출하지 않아 서비스 속도 향상을 기대할 수 있었기 때문이에요. 하지만 벌크 API를 제공하지 않아 해당안으로는 진행할 수 없었어요.</p>
<p>다음 안으로는 비동기 API호출이었어요. 한 페이지마다 API Response를  대기하지 않고 비동기로 호출한다면 벌크 API와 비슷한 성능을 기대할 수 있었어요. 따라서 <strong><em>비동기 API를 호출하여 모든 페이지의 Response가 올 때 까지 대기 이후 데이터를 처리하는 방향으로 진행하기로 결정했어요.</em></strong></p>
<h3 id="기존-코드">기존 코드</h3>
<p><em>기존의 코드는 다음과 같은 형태를 띄고 있었어요.</em></p>
<pre><code class="language-java">import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.assertThat;

public class AsyncCallTest {
    private static class Response {}

    private static class ApiResponse {
        int totalPage;
        int currentPage;

        public ApiResponse(int currentPage, int totalPage) {
            this.currentPage = currentPage;
            this.totalPage = totalPage;
        }
    }

    private static final Random RANDOM = new Random();

    @Test
    void asIs() {
        List&lt;Response&gt; result = new ArrayList&lt;&gt;();
        List&lt;ApiResponse&gt; apiResponses = new ArrayList&lt;&gt;();
        final int START_PAGE = 1;

        // 최초 1회 API 호출하여 전체 페이지 수 획득
        ApiResponse firtApiResponse = this.apiCall(START_PAGE);
        apiResponses.add(firtApiResponse);

        // 전체 페이지 수 만큼 API 호출
        for (int i = START_PAGE + 1; i &lt;= firtApiResponse.totalPage; i++) {
            apiResponses.add(this.apiCall(i));
        }

        // 승인 대사 진행
        for (ApiResponse apiResponse : apiResponses) {
            result.add(this.logic(apiResponse));
        }

        assertThat(result).hasSize(10);
    }

    private ApiResponse apiCall(int page) {
        try {
            Thread.sleep(RANDOM.nextInt(1000, 3000));
            final int TOTAL_PAGE = 10;
            return new ApiResponse(page, TOTAL_PAGE);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private Response logic(ApiResponse apiResponse) {
        // 승인 대사 진행 DB 작업 (Insert, Update)
        return new Response();
    }
}</code></pre>
<pre><code class="language-java">✅ Tests passed: 1 of 1 test - 20 sec 59 ms

[API 호출] Current Page: 1 Thread: Test worker
[API 호출] Current Page: 2 Thread: Test worker
[API 호출] Current Page: 3 Thread: Test worker
[API 호출] Current Page: 4 Thread: Test worker
[API 호출] Current Page: 5 Thread: Test worker
[API 호출] Current Page: 6 Thread: Test worker
[API 호출] Current Page: 7 Thread: Test worker
[API 호출] Current Page: 8 Thread: Test worker
[API 호출] Current Page: 9 Thread: Test worker
[API 호출] Current Page: 10 Thread: Test worker
[승인 대사 진행] Current Page: 1 Thread: Test worker
[승인 대사 진행] Current Page: 2 Thread: Test worker
[승인 대사 진행] Current Page: 3 Thread: Test worker
[승인 대사 진행] Current Page: 4 Thread: Test worker
[승인 대사 진행] Current Page: 5 Thread: Test worker
[승인 대사 진행] Current Page: 6 Thread: Test worker
[승인 대사 진행] Current Page: 7 Thread: Test worker
[승인 대사 진행] Current Page: 8 Thread: Test worker
[승인 대사 진행] Current Page: 9 Thread: Test worker
[승인 대사 진행] Current Page: 10 Thread: Test worker</code></pre>
<p>실제 로직은 승인 대사 데이터 기간을 설정하고 API 호출에 필요한 Header를 셋팅 하는 등 더 복잡하지만, 간략하게 핵심 내용만 추리기 위해 작성해보았어요.</p>
<p><code>apiCall</code>을 통해 승인대사 API를 호출하면, 1~3초 정도 걸린다는 가정하에 작성하였으며, <code>ApiResponse</code>를 통해 전체 페이지 수가 얼마인지 표기해주어요. 현재 코드에서는 10페이지를 가정하여 작성했어요.</p>
<p>최초 1회 API 호출을 통해 전체 페이지 수를 얻고, 반복문을 통해 API를 호출해요. 이 경우 최악의 경우에 모든 API가 3초의 소요시간이 걸린다면 서비스 시간은 API 호출 30초 + 승인대사 데이터 처리 시간으로 30초 이상 걸릴 수 있어요.</p>
<p>따라서 <strong><em>반복문을 통해 승인 대사를 진행하는 부분을 비동기 처리로 개선</em></strong>하고자 하였어요.</p>
<h3 id="개선-코드">개선 코드</h3>
<pre><code class="language-java">@Test
void toBe() {
    List&lt;Response&gt; result = this.asyncApiCall() // 비동기 API 호출
            .stream()
            .map(this::logic) // 승인 대사 진행
            .collect(Collectors.toList());;

    assertThat(result).hasSize(10);
}

private List&lt;ApiResponse&gt; asyncApiCall() {
    final int START_PAGE = 1;
    final int SECOND_PAGE = START_PAGE + 1;

    // 최초 1회 API 호출하여 전체 페이지 수 획득
    ApiResponse firtApiResponse = this.apiCall(START_PAGE);

    // 전체 페이지 수 만큼 API 호출
    List&lt;ApiResponse&gt; apiResponses = IntStream
            .rangeClosed(SECOND_PAGE, firtApiResponse.totalPage)
            .boxed()
            .parallel()
            .map(this::apiCall)
            .collect(Collectors.toList());

    apiResponses.add(firtApiResponse);
    return apiResponses;
}</code></pre>
<pre><code class="language-java">✅ Tests passed: 1 of 1 test - 6 sec 102 ms

[API 호출] Current Page: 1 Thread: Test worker
[API 호출] Current Page: 7 Thread: Test worker
[API 호출] Current Page: 2 Thread: ForkJoinPool.commonPool-worker-8
[API 호출] Current Page: 4 Thread: ForkJoinPool.commonPool-worker-1
[API 호출] Current Page: 5 Thread: ForkJoinPool.commonPool-worker-3
[API 호출] Current Page: 9 Thread: ForkJoinPool.commonPool-worker-4
[API 호출] Current Page: 10 Thread: ForkJoinPool.commonPool-worker-6
[API 호출] Current Page: 6 Thread: ForkJoinPool.commonPool-worker-7
[API 호출] Current Page: 8 Thread: ForkJoinPool.commonPool-worker-5
[API 호출] Current Page: 3 Thread: ForkJoinPool.commonPool-worker-2
[승인 대사 진행] Current Page: 2 Thread: Test worker
[승인 대사 진행] Current Page: 3 Thread: Test worker
[승인 대사 진행] Current Page: 4 Thread: Test worker
[승인 대사 진행] Current Page: 5 Thread: Test worker
[승인 대사 진행] Current Page: 6 Thread: Test worker
[승인 대사 진행] Current Page: 7 Thread: Test worker
[승인 대사 진행] Current Page: 8 Thread: Test worker
[승인 대사 진행] Current Page: 9 Thread: Test worker
[승인 대사 진행] Current Page: 10 Thread: Test worker
[승인 대사 진행] Current Page: 1 Thread: Test worker</code></pre>
<p><code>Parallel Stream</code>을 이용하여 비동기로 API를 호출했어요. Java의 Parallel Stream은 내부적으로 <code>ForkJoinPool</code>의 <code>commonPool</code>을 사용해요. ForkJoinPool은 <code>ExecutorService</code>인터페이스를 상속받고 있어요. 내부적으로 Task의 개수(크기, 사이즈)에 따라 분할(fork)하여 Task을 처리하고 모든 Task가 처리되었을 경우에는 Join을 통해 취합해요. ForkJoinPool의 또 다른 특징으로는 유휴(놀고 있는) Thread가 있을 경우 유휴 Thread가 다른 Thread 잡들을 가져와 수행하여 최대한 유휴 Thread가 없이 모두 일관된 개수의 Task를 처리해요.</p>
<p>ForkJoinPool은 수행되야하는 Task가 많아질 수록 <code>Worker Thread</code>가 추가되어요. 따라서 만약 ForkJoinPool의 Thread 개수를 제한하려면 다음과 같이 직접 ForkJoinPool을 생성하여 사용할 수도 있어요.</p>
<pre><code class="language-java">private static final ForkJoinPool EXECUTOR = new ForkJoinPool(10);</code></pre>
<p>ForkJoinPool의 <code>parallelism</code> 파라미터에 int값으로 원하는 Thread 수 개수를 인자로 넘겨주면 해당 개수 만큼의 Thread만 생성이되어요.</p>
<pre><code class="language-java">public class ForkJoinPoolTest {
    private static final ForkJoinPool EXECUTOR = new ForkJoinPool(10);

    @Test
    void test() {
        EXECUTOR.submit(() -&gt; IntStream.rangeClosed(1, 30)
                .boxed()
                .parallel()
                .peek(i -&gt; {
                    try {
                        Thread.sleep(new Random().nextInt(1000, 3000));
                        System.out.println(&quot;[NUMBER: &quot; + i + &quot;] Thread: &quot; + Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                })
                .collect(Collectors.toList())
        ).join();

    }
}</code></pre>
<pre><code class="language-java">[NUMBER: 30] Thread: ForkJoinPool-1-worker-7
[NUMBER: 25] Thread: ForkJoinPool-1-worker-6
[NUMBER: 22] Thread: ForkJoinPool-1-worker-9
[NUMBER: 27] Thread: ForkJoinPool-1-worker-10
[NUMBER: 28] Thread: ForkJoinPool-1-worker-3
[NUMBER: 20] Thread: ForkJoinPool-1-worker-1
[NUMBER: 17] Thread: ForkJoinPool-1-worker-4
[NUMBER: 16] Thread: ForkJoinPool-1-worker-5
[NUMBER: 10] Thread: ForkJoinPool-1-worker-2
[NUMBER: 5] Thread: ForkJoinPool-1-worker-8
[NUMBER: 29] Thread: ForkJoinPool-1-worker-7
[NUMBER: 21] Thread: ForkJoinPool-1-worker-9
[NUMBER: 19] Thread: ForkJoinPool-1-worker-10
[NUMBER: 26] Thread: ForkJoinPool-1-worker-6
[NUMBER: 2] Thread: ForkJoinPool-1-worker-1
[NUMBER: 24] Thread: ForkJoinPool-1-worker-5
[NUMBER: 4] Thread: ForkJoinPool-1-worker-8
[NUMBER: 1] Thread: ForkJoinPool-1-worker-4
[NUMBER: 18] Thread: ForkJoinPool-1-worker-3
[NUMBER: 11] Thread: ForkJoinPool-1-worker-2
[NUMBER: 6] Thread: ForkJoinPool-1-worker-6
[NUMBER: 7] Thread: ForkJoinPool-1-worker-10
[NUMBER: 3] Thread: ForkJoinPool-1-worker-9
[NUMBER: 23] Thread: ForkJoinPool-1-worker-7
[NUMBER: 15] Thread: ForkJoinPool-1-worker-4
[NUMBER: 13] Thread: ForkJoinPool-1-worker-5
[NUMBER: 14] Thread: ForkJoinPool-1-worker-1
[NUMBER: 9] Thread: ForkJoinPool-1-worker-8
[NUMBER: 8] Thread: ForkJoinPool-1-worker-2
[NUMBER: 12] Thread: ForkJoinPool-1-worker-3</code></pre>
<p><em>Worker Thread가 10개 이상 늘어나지 않는 것을 확인할 수 있어요.</em> </p>
<p>이 처럼 이번 개선에서는 ForkJoinPool의 Thread 수를 제한하여 사용하는 방법으로 개선할 수 있었어요. </p>
<h3 id="왜-forkjoinpool을-사용하였는가">왜 ForkJoinPool을 사용하였는가?</h3>
<ol>
<li><p>격리된 ThreadPool 환경에서 실행되어 다른 로직들에 의한 영향을 받지 않기 위함.</p>
<p> Custom한 Executor를 사용할 수도 있었지만, 승인 대사 로직이 다른 로직에서 모두 사용되는 공통된 Executor에서 사용되기보다 격리된 환경에서 실행되어 다른 로직에 영향을 받지 않기 위함이 있었어요. 공통된 Executor를 사용할 경우 특정한 로직에서 Thread를 잡고 놓아주지 않거나, 혹은 Exeption이 발생한다면 다른 로직에도 영향이 갈 것으로 판단되었어요(<em>다른 개발자가 공통된 Executor 사용 도중 적절한 예외처리를 하지 못하였거나, 예상치 못한 엑셉션이 발생될 경우</em>). Thread 작업 시간을 한정 짓고 해당 작업시간을 초과하면 반환하도록 구현하고 Exception을 Handling 하여 구현할 수도 있었지만, 가장 큰 이유는 격리된 환경에서 승인 대사가 이루어지는 것을 원했어요.</p>
</li>
</ol>
<ol start="2">
<li><p>승인 대사 서버에서 비동기 작업이 필요한 로직은 위의 특정한 승인 대사에 거의 한정됨.</p>
<p> 승인 대사 서버에서 비동기 작업이 필요한 로직은 많지 않았어요. 따라서 Custom한 Executor를 개발하는 것보다 빠르고 간편하게 ForkJoinPool을 이용하는 방안을 채택하게 되었어요. 만약 비동기 작업이 많이 필요로하는 서비스였다면, <code>Spring MVC</code>가 아니라 <code>Spring WebFlux</code>를 채택하는 방안이나 CustomExecutor를 개발하는 방향으로 갔을 거예요. 하지만 이는 모두 개발 Cost로 이어지죠. 이러한 다양한 비용을 생각해보고 ForkJoinPool을 사용하기로 결정했어요.</p>
</li>
</ol>
<h2 id="결과">결과</h2>
<h3 id="1-전체-승인-대사-api-속도">1. 전체 승인 대사 API 속도</h3>
<p>동일 시간 대 Matrics - <strong><em>개선 결제 수단 뿐만 아니라 다른 결제 수단의 승인 대사도 포함된 수치</em></strong></p>
<p>비교 시간 대</p>
<ul>
<li><p>AS-IS Time: 2023-10-11 09:08:00 ~ 2023-10-12 09:08:00</p>
</li>
<li><p>TO-BE Time: 2023-10-18 09:08:00 ~ 2023-10-19 09:08:00</p>
</li>
</ul>
<p>AS-IS</p>
<ul>
<li>평균시간: 4352.689</li>
<li>최소시간: 30</li>
<li>최대시간: 28071</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/9302352b-c403-4ef0-9e51-1205a55da2b3/image.png" alt=""></p>
<p>TO-BE</p>
<ul>
<li>평균시간: 2906.083</li>
<li>최소시간: 24</li>
<li>최대시간: 23139</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/d4517000-9283-404e-970d-2186fb9f7f1a/image.png" alt=""></p>
<h3 id="2-180000-동일-시간-대-n페이-승인-대사-속도">2. 18:00:00 동일 시간 대 N페이 승인 대사 속도</h3>
<p>AS-IS 서비스 시간: 22838ms</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/3e166f62-577f-453e-95d4-615595802df6/image.png" alt=""></p>
<p>TO-BE 서비스 시간: 5354ms</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/400b4654-9b12-4799-a8c3-792da1208705/image.png" alt=""></p>
<h3 id="3-승인-대사-서버-cpu-및-메모리-사용량">3. 승인 대사 서버 CPU 및 메모리 사용량</h3>
<p>As-Is</p>
<ul>
<li>최고 CPU 사용량: 3194.222</li>
<li>최고 메모리 사용량: 11226</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/fd2542b4-e7f4-4b70-8e3e-b3e0f595e83a/image.png" alt=""></p>
<p>To-Be</p>
<ul>
<li>최고 CPU 사용량: 2242.222</li>
<li>최고 메모리 사용량: 7569.286</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/81096d2f-8706-43d6-8831-56e38328c932/image.png" alt=""></p>
<h2 id="결과-정리">결과 정리</h2>
<h3 id="1-전체-승인-대사-api">1. 전체 승인 대사 API</h3>
<ul>
<li>평균시간: <strong>33.16% 감소</strong></li>
</ul>
<h3 id="2-승인-대사-성능">2. 승인 대사 성능</h3>
<ul>
<li>서비스 성능: <strong>76.55% 향상</strong></li>
</ul>
<h3 id="3-cpu-및-메모리-사용량">3. CPU 및 메모리 사용량</h3>
<ul>
<li>CPU 사용량: <strong>31.98% 감소</strong></li>
<li>메모리 사용량: <strong>32.62% 감소</strong></li>
<li>일평균 메모리 사용량: <strong>48.12% 감소</strong></li>
</ul>
<h2 id="마치며">마치며</h2>
<p>많은 시간을 소비하고 있던 특정 결제의 승인 대사를 개선해 보았어요. 이번 개선을 통해 단 하나의 승인 대사만 개선했음에도 불구하고, CPU 및 메모리 사용량을 크게 감소 시킬 수 있었으며, N페이 승인 대사 속도의 경우에는 약 77%의 성능 향상을 이룰 수 있었어요.</p>
<p>가장 놀라운 점은 CPU와 메모리 사용량도 크게 감소되었다는 것이었어요. 이는 곧 서비스 비용 절감에도 이점을 취할 수 있는 부분이라 매우 기쁘게 여겨져요.</p>
<p>이번 개선을 통해 <strong><em>하나의 서비스가 전체 서비스에 미치는 영향</em></strong>이 이렇게 클 수 있구나 몸소 깨달을 수 있었어요. 따라서 하나의 서비스, 로직을 작성하더라도 단순하게 기능이 동작하게 만드는 것이 전부가 아닌 내가 작성한 코드로 인한 서비스 영향도 파악 또한 중요하다는 것을 배웠어요.</p>
<p>성능 개선의 답이 항상 비동기라고 생각하지는 않아요. 하지만 상황에 맞게 사용할 경우 큰 이점을 얻을 수 있다고 생각해요. 비동기적인 접근법 이외에도 코드 최적화, 캐싱, 데이터베이스 쿼리 튜닝 등 다양한 기법이 존재하니 상황에 맞게 적용하는 것이 중요한 거 같아요.</p>
<p>앞으로도 모니터링하고 분석하여 성능 개선이 필요한 부분을 찾아 개선하기 위해 꾸준히 노력하고 학습할 예정이예요😆.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🐛 [트러블슈팅] B2C 결제 cross-origin 이슈 해결 feat. window.postMessage()]]></title>
            <link>https://velog.io/@dev_lee/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EA%B2%B0%EC%A0%9C-cross-origin-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_lee/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EA%B2%B0%EC%A0%9C-cross-origin-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 23 Jan 2024 14:19:42 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<pre><code class="language-jsx">Uncaught DOMException: Blocked a frame with origin &quot;https://pac.domain.com&quot; 
from accessing a cross-origin frame.</code></pre>
<pre><code class="language-jsx">Uncaught SecurityError: Failed to read a named property &#39;parentSomethingProperty&#39; 
on &#39;Window&#39;: Blocked a frame with origin &quot;https://pac.domain.com&quot; 
from accessing a cross-origin frame.</code></pre>
<p>정상적으로 진행되던 결제에서 갑자기 위와 같은 에러로 결제가 불가능한 상황이 발생했어요. 코드가 변경된 것도 없었는데 갑자기 왜 이런 에러가 발생되었을까요?</p>
<h2 id="원인-분석">원인 분석</h2>
<p>위의 에러 메시지를 확인해보면 <a href="https://pac.domain.com">https://pac.domain.com</a> 주소의 접근이 차단되었다는 것을 확인해볼 수 있어요. 여기서 포인트는 <code>cross-orgin</code>이라는 메시지에요.</p>
<p><code>cross-origin</code>이란 교차 출처를 뜻하고 <code>CORS(Cross Origin Resource Sharing)</code>란 교차 출처에서의 자원 공유를 뜻해요. 그렇다면 출처란 무엇일까요?</p>
<h3 id="출처의-개념">출처의 개념</h3>
<pre><code class="language-jsx">https://leeseo.mydomain.com:443/post/search?page=1#CORS란</code></pre>
<ul>
<li><code>https://</code>: Protocol</li>
<li><code>leeseo.mydomain.com</code>: Host</li>
<li><code>443</code>: Port(생략 가능)</li>
<li><code>/post/search</code>: Path</li>
<li><code>?page=1</code>: Query String</li>
<li><code>#CORS란</code>: Fragment</li>
</ul>
<p>위의 URL에서 <code>Protocol</code>, <code>Host</code>, <code>Port</code> 이 3가지가 동일할 때 <code>동일 출처(Same-origin)</code>라고 해요.</p>
<p>만약 Protocol, Host, Port 중 단 하나라도 다르다면 <code>교차 출처(Cross-origin)</code>로 간주돼요.</p>
<h3 id="서버에서-cors-설정은-이미-존재">서버에서 CORS 설정은 이미 존재</h3>
<p>기존 서버측 코드에서는 CORS에 관한 코드가 이미 작성되어 있었어요. 그럼에도 불구하고 위와 같은 교차 출처 문제가 발생되고 있었어요.</p>
<p>우리가 많이 접하는 CORS는 프론트와 백엔드 서버의 통신에서 교차 출처로 인해 발생되는 이슈가 많았어요. 이로인해 CORS와 관련된 검색을 하면 대부분 프론트와 서버의 통신에서 서버에 CORS 해제 방법과 관련한 정보만 많이 나와 해당 이슈의 원인을 파악하는데 어려움이 있었어요.</p>
<p>저희는 스프링 부트(Spring Boot)와 스프링 시큐리티(Spring Security)를 사용 중이었는데 해당 관련 정보도 전부 스프링 시큐리티에서 CORS 관련 설정하는 방법이나 스프링에서 지원하는 <code>@CrossOrigin</code>을 활용하는 방법만 나왔어요.</p>
<p>따라서 관점을 달리하고 처음으로 돌아가 해당 문제에 대한 근본적인 원인을 다시 파악해보기로 했어요.</p>
<h3 id="코드-분석">코드 분석</h3>
<p>원인을 분석할 때 교차 출처라는 에러 메시지의 내용만으로 판단하여 해결하려 했던 것이 패착이었어요. 따라서 기존 코드를 다시 분석하기로 했어요.</p>
<p>저희의 결제 시스템은 아래와 같은 시퀀스로 구성되어 있어요. 이해를 돕기 위해 Path나 관련 데이터 세팅 및 예외처리 등은 생략하도록 할게요.</p>
<ol>
<li>mydomain.com에서 팝업을 오픈해요.</li>
</ol>
<pre><code class="language-jsx">// 우선 빈 팝업을 오픈해요.
window.open(&#39;about:blank&#39;, &#39;pay-popup&#39;, &#39;width=600,height=400&#39;);

// 자바스크립트를 통해 동적으로 만들어 호출할 수도 있어요.
// 이해를 돕기 위해 html로 작성할게요.
// 1차적으로 결제와 관련한 데이터를 셋팅해요.
// 저희 결제 서버에 데이터를 전송하는 form을 작성해요.
&lt;form id=&quot;pay&quot; action=&quot;https://pac.mydomain.com/requestPay&quot; style=&quot;display: none;&quot;&gt;
    &lt;input id=&quot;info1&quot; name=&quot;info1&quot; value=&quot;info1&quot; /&gt;
&lt;/form&gt;

// pay-popup이 pac.mydomain.com으로 이동돼요.
document.getElementByid(&quot;pay&quot;).submit();</code></pre>
<ol start="2">
<li>pac.mydomain.com 서버에서 결제 관련 데이터를 세팅하고 결제 벤더사에 요청해요.</li>
</ol>
<pre><code class="language-jsx">&lt;form id=&quot;pay-request&quot; action=&quot;https://www.payment.com/blabla&quot; style=&quot;display: none;&quot;&gt;
    // 결제와 관련된 데이터 셋팅
    &lt;input id=&quot;returnUrl&quot; 
           name=&quot;returnUrl&quot; 
              value=&quot;https://pac.mydomain.com/returnPay&quot; 
    /&gt;
&lt;/form&gt;

// 폼 방식으로 현재 팝업에서 벤더사에 결제 페이지를 요청(submit)해요.
document.getElementById(&#39;pay-request&#39;).submit();</code></pre>
<ol start="3">
<li>고객이 결제 벤더사 페이지에서 결제를 완료하면 returnUrl에 따라 저희 서버로 다시 요청해요.</li>
<li>pac.mydomain.com 서버에서 벤더사의 response를 받아 성공일 경우 부모 페이지의 결제 성공 함수를 호출해요.</li>
</ol>
<pre><code class="language-jsx">// 결제 응답이 성공일 경우 mydomain.com에 있는
// successPayment를 호출하여 결제 완료 페이지로 이동해요.
parent.successPayment(data);</code></pre>
<p>문제는 4번에서 발생되었어요. 만약 프론트와 서버의 교차 출처 문제였다면 결제 서버에 존재하는 스프링 시큐리티에서 CORS 관련 해제 코드로 해결이 되었을 거에요. 하지만 위의 문제는 프론트와 백엔드 통신에서 발생하는 CORS 문제가 아니라 팝업에서 부모 페이지의 자원에 접근할 때 발생되는 교차 출처 문제였어요.</p>
<p>우리 홈페이지(부모 페이지)는 <code>mydomain.com</code>인데 결제 서버(팝업)는 <code>pac.mydomain.com</code>으로 구성되어 있기 때문에 교차 출처가 발생되어 팝업에서 부모 페이지에 있는 <code>successPayment</code> 함수를 호출하지 못하는 것이었죠. (MSA 구조로 홈페이지 서버와 결제 서버가 분리되어 있어요.)</p>
<p>하지만 왜 갑자기 교차 출처 문제가 발생되었을까요? 기존에는 왜 정상적으로 결제가 진행 되었을까요?</p>
<h3 id="기존-코드">기존 코드</h3>
<p>기존 코드에서는 교차 출처 문제를 해결하기 위해 결제 서버의 자바스크립트에서 <code>document.domain</code>의 Setter를 통해 해결하고 있었어요. </p>
<p><strong><code>document.domain</code></strong> 속성은 도메인 이름을 설정하거나 가져올 수 있는 DOM 속성이에요. 이를 통해 부모-자식 창 간에 서로 다른 도메인을 동일한 도메인으로 설정함으로써 동일 출처 정책(Same-Origin Policy) 제한을 피할 수 있었어요. 아래와 같은 코드로 말이죠.</p>
<pre><code class="language-jsx">docuemnt.domain = &quot;mydomain.com&quot;</code></pre>
<p>위와 같이 domain을 “mydomain.com” 으로 수정하게 되면 서로 다른 서브 도메인(Subdomain)을 가지지만 동일한 eTLD+1(mydomain.com)을 가지게 되어 두 출처를 동일 출처인 것처럼 취급이 가능했어요.</p>
<ul>
<li>도메인: pac.mydomain.com</li>
<li>eTLD(Effective Top-Level Domain): .com</li>
<li>eTLD+1: mydomain.com</li>
</ul>
<p>단, 위의 경우에도 기본 도메인 자체가 다른 경우에는 적용이 불가능해요.</p>
<p>예1. ) pac.mydomain.com 을 <code>documnet.domain = “mydomain.com”</code> 으로 변경 가능</p>
<p>예2. ) pac.super-mydomain.com을 <code>document.domain = “mydomain.com”</code> 으로 변경 불가</p>
<p>예3. ) super-mydomain.com을 <code>document.domain = “mydomain.com”</code> 으로 변경 불가</p>
<p>하지만 document.domain setter는 아래와 같은 문제점을 가지고 있어요.</p>
<h3 id="documentdomain-setter-문제점">document.domain Setter 문제점</h3>
<ol>
<li>도메인의 포트 번호를 무시해요.</li>
<li>도메인을 변경하여 동일 출처 페이지처럼 접근하여 정보를 탈취할 수 있어요.</li>
</ol>
<h3 id="documentdomain-setter-지원중단deprecated">document.domain Setter 지원중단(Deprecated)</h3>
<p>이러한 문제점으로 인해 Chrome 115 버전 부터는 document.domain을 변경할 수 없게 되었어요. 크롬뿐만 아니라 다른 브라우저도 점차 해당 기능을 제공하지 않을 예정이에요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/a77c2753-e468-4728-820d-a7c4b02e5fce/image.png" alt="chrome deprecated"></p>
<p><em>Chrome 115부터 지원 중단</em></p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/1fc8fe56-f70e-4ba3-b19a-af40eba08e49/image.png" alt="mdn deprecated"></p>
<p><em>MDN Deprecated</em></p>
<p>이렇게 점차 브라우저에서 업데이트됨에 따라 document.domain Setter가 동작하지 않게 되었고, 이로 인해 어떤 고객은 정상적으로 진행되고 최신 브라우저 버전을 사용하는 고객은 정상 진행이 불가능했던 것이었어요. 누구는 정상 진행이 되고, 누구는 정상 진행이 불가능했던 이유가 여기에 있었던 것이었죠.</p>
<p>사내PC에서 해당 현상 구현과 파악이 어려웠던 이유는 보안 정책상 폐쇄망으로 이루어져 구버전 브라우저를 이용했기 때문에 원인을 모르는 상태에서 이슈 파악에 더 어려웠다고 사료돼요. 이로 인해 외부망에서만 해당 현상이 구현되는 이유도 설명이 되었죠. (2024.01.03 외부망 Chrome Version: 120.0.6099.130)</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/d7fa46f7-3ebb-4527-830f-2406ae402890/image.png" alt=""></p>
<p><em>폐쇄망으로 인한 Google Chrome 자동 및 수동 업데이트 불가능한 상태.</em></p>
<h2 id="해결">해결</h2>
<p>이러한 교처 출처에서 안전하게 동일 출처 정책 제약 조건을 우회할 수 있는 기능을 자바스크립트에서는 <strong><code>window.postMessage()</code></strong>을 통해 제공하고 있어요. 해당 함수는 부모 페이지에서 생성된 팝업 간의 통신이나, 페이지와 페이지 안의 iframe 간의 통신에서 사용할 수 있어요.</p>
<h3 id="windowpostmessage-사용-방법">window.postMessage() 사용 방법</h3>
<ul>
<li>수신 부분</li>
</ul>
<pre><code class="language-jsx">// 방법 1.
window.onmesage = (event) =&gt; {
    console.log(event);
};

// 방법 2.
window.addEventListener(&#39;message&#39;, (event) =&gt; {
    console.log(event);
});

// 방법 3. Object data를 받는 방법
window.onmessage = (event) =&gt; {
    console.log(JSON.parse(event.data));
};</code></pre>
<p>event 객체 프로퍼티</p>
<ol>
<li><strong><code>data</code></strong>: 전달 받은 데이터.</li>
<li><strong><code>origin</code></strong>: postMessage 가 호출될 때 메시지를 보내는 윈도우의 origin.</li>
<li><strong><code>source</code></strong>: 메시지를 보낸 window 오브젝트에 대한 참조.</li>
</ol>
<ul>
<li>발신 부분</li>
</ul>
<pre><code class="language-jsx">// 기본 문법
targetWindow.postMessage(message, targetOrigin, [transfer]);

// string 전송
targetWindow.postMessage(&#39;message&#39;);

// 객체 전송
const data = { message: &#39;my message&#39; };
targetWindow.postMessage(JSON.stringify(data));

// targetOrigin 지정
targetWindow.postMessage(&#39;message&#39;, &#39;https://mydomain.com&#39;);</code></pre>
<ol>
<li><strong><code>targetWindow</code></strong>: 메시지를 전달 받을 window의 참조.<ul>
<li>window.opener: 새 창을 만든 window를 참조할 때.</li>
<li>window.parent: 임베디드된 iframe 혹은 팝업에서 한번 더 submit을 통해 렌더링 된 경우 submit한 페이지의 window 객체를 참조할 때.</li>
</ul>
</li>
<li><strong><code>message</code></strong>: 다른 window에 보내질 데이터.</li>
<li><strong><code>targetOrigin</code></strong>: targetWindow의 origin을 지정. ‘*’일 경우 별도로 지정하지 않음을 의미.<ul>
<li>패스워드와 같은 중요한 데이터의 경우 반드시 지정하여 악의적인 제 3자가 가로채지 못하도록 설정하는 것이 매우 중요해요.</li>
</ul>
</li>
</ol>
<ul>
<li>코드 적용 예시</li>
</ul>
<pre><code class="language-jsx">// 부모 페이지
const popup = window.open(&#39;about:blank&#39;, &#39;pay-popup&#39;, &#39;width=600,height=400&#39;);

// 데이터 셋팅 후 submit, 포스팅 상단 부분 예시 참조

window.onmessage((event) =&gt; {
    if (event.origin !== &#39;https://pac.mydomain.com&#39;) return;

    const { code, data } = JSON.parse(event.data);
    if (code !== &#39;success&#39;) return;
    successPayment(data);
});

function successPayment(data) {
    // 결제 후처리 로직
}

// 참고. 만약 부모 페이지에서 팝업에 메시지를 전달하고 싶을 경우.
// popup.postMessage(&#39;message&#39;);</code></pre>
<pre><code class="language-jsx">// 팝업
const data = {
    code: &#39;success&#39;,
    data: { ... }, // 생략
};

opener.postMessage(JSON.stringify(data));</code></pre>
<p><em><code>window.postMessage()</code> 사용 방법에 대해서 더욱 자세한 설명은 아래 링크에서 확인 가능해요.</em></p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage">Window.postMessage() - Web API | MDN</a></p>
<h2 id="결과">결과</h2>
<h3 id="as-is">As-Is</h3>
<p>하루 평균 약 2,000 ~ 3,000 건 로그 적재</p>
<p>2024.01.02 00:00:00 ~ 2024:01.02 23:59:59 기준: 3,074 건</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/224ccec5-126b-4972-a5e1-950d33f824d2/image.png" alt="kibana_as-is"></p>
<h3 id="to-be">To-Be</h3>
<p>2024.01.03 17:00:00 ~ 2024:01.04 15:00:00 기준: 0건</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/7190fe1b-1096-477b-b5da-453d8c6add5c/image.png" alt="kibana_to-be"></p>
<h4 id="참고-자료">참고 자료</h4>
<p><em>아래 링크에서 포스팅에서 다루었던 정보를 보다 자세하게 확인할 수 있어요.</em></p>
<ol>
<li>Chrome 버전 업데이트</li>
</ol>
<ul>
<li><a href="https://chromiumdash.appspot.com/schedule?hl=ko">Chromium Dash</a></li>
</ul>
<ol start="2">
<li>Chrome document.domain deprecated 관련 블로그 포스트</li>
</ol>
<ul>
<li><p><a href="https://developer.chrome.com/blog/immutable-document-domain">Chrome will disable modifying document.domain to relax the same-origin policy</a></p>
</li>
<li><p><a href="https://developer.chrome.com/blog/document-domain-setter-deprecation">Chrome disables modifying document.domain</a></p>
</li>
</ul>
<ol start="3">
<li>URL 구조 관련 포스트</li>
</ol>
<ul>
<li><p><a href="https://web.dev/articles/same-site-same-origin">Understanding &quot;same-site&quot; and &quot;same-origin&quot;</a></p>
</li>
<li><p><a href="https://www.beusable.net/blog/?p=4507">데이터 분석을 위한 기초, URL 이해하기 | 뷰저블</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🐛 [Spring Security] H2 console 활성화 시 UnsatisfiedDependencyException 발생 이슈 해결]]></title>
            <link>https://velog.io/@dev_lee/Spring-Security-H2-console-%ED%99%9C%EC%84%B1%ED%99%94-%EC%8B%9C-UnsatisfiedDependencyException-%EB%B0%9C%EC%83%9D-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_lee/Spring-Security-H2-console-%ED%99%9C%EC%84%B1%ED%99%94-%EC%8B%9C-UnsatisfiedDependencyException-%EB%B0%9C%EC%83%9D-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 17 Oct 2023 12:09:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/6b63f690-9216-4b32-a153-62368abaabcd/image.jpg" alt=""></p>
<h2 id="개요">개요</h2>
<p><code>spring.h2.console.enabled: true</code>로 설정하면서 Bean이 정상적으로 생성되지 않는 이슈가 발생되었어요. </p>
<p>Spring Boot가 3점대로 버전이 업그레이드 되면서 Spring Security 의존성 버전도 함께 올라갔어요. <code>2.7.X</code> 기준으로 Spring Security는 <code>5.7.11</code> 버전이었지만, Spring Boot <code>3.1.4</code> 기준으로 Spring Security는 <code>6.1.4</code> 버전을 의존성 주입받고 있어요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/ae40f5dd-0784-41ec-ae23-81b1cc73b3d2/image.png" alt=""></p>
<p><a href="https://docs.spring.io/spring-boot/docs/2.7.x/reference/html/dependency-versions.html">Spring Boot 2.7.X Dependency Versions</a></p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/f2e94696-ad11-448e-b42f-ee2b30c77611/image.png" alt=""></p>
<p><a href="https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html">Spring Boot 3.1.4 Dependency Versions</a></p>
<br />
<br />

<p>이에 따라 Security에도 기존의 코드들이 Deprecated가 된 경우가 많으며, Security의 개념은 같지만 메서드명 혹은 상속받는 구조 등 많은 부분에서 변화가 생겼어요. 이에 기존 2점대 버전에서는 정상적으로 동작하던 것들도 3버전대로 가면서 새롭게 마이그레이션 해주어야 하는 부분이 생겼어요.</p>
<p><em>H2 Console 활성화</em></p>
<pre><code class="language-jsx">spring:
  h2:
    console:
      enabled: true</code></pre>
<h2 id="개발-환경">개발 환경</h2>
<ul>
<li><strong>Spring Boot</strong>: <code>3.1.4</code></li>
<li>Spring Security: <code>6.1.4</code></li>
<li><strong>Language</strong>: <code>Java 17</code></li>
<li><strong>Build Tool</strong>: <code>Gradle 8.3</code></li>
</ul>
<h2 id="에러-코드">에러 코드</h2>
<pre><code class="language-java">@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class SecurityConfig {
    private static final String[] WHITE_LIST = {
            &quot;/helloWorld&quot;,
    };

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .headers(header -&gt; header.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
                .authorizeHttpRequests(request -&gt; request
                        .requestMatchers(PathRequest.toH2Console()).permitAll()
                        .requestMatchers(WHITE_LIST).permitAll()
                        .anyRequest().authenticated())
                .build();
    }
}</code></pre>
<h2 id="에러-로그">에러 로그</h2>
<pre><code class="language-javascript">org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name &#39;org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration&#39;: Unsatisfied dependency expressed through method &#39;setFilterChains&#39; parameter 0: Error creating bean with name &#39;securityFilterChain&#39; defined in class path resource [com/demo/springdemo/global/security/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method &#39;securityFilterChain&#39; threw exception with message: This method cannot decide whether these patterns are Spring MVC patterns or not. If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).

This is because there is more than one mappable servlet in your servlet context: {org.h2.server.web.JakartaWebServlet=[/h2-console/*], org.springframework.web.servlet.DispatcherServlet=[/]}.

For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path.
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:875) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:828) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:492) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1416) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:597) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:942) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:608) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298) ~[spring-boot-3.1.4.jar:3.1.4]
    at com.demo.springdemo.SpringDemoApplication.main(SpringDemoApplication.java:10) ~[main/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:50) ~[spring-boot-devtools-3.1.4.jar:3.1.4]

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name &#39;securityFilterChain&#39; defined in class path resource [com/demo/springdemo/global/security/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method &#39;securityFilterChain&#39; threw exception with message: This method cannot decide whether these patterns are Spring MVC patterns or not. If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).</code></pre>
<h2 id="원인">원인</h2>
<p><code>org.h2.server.web.JakartaWebServlet</code>과 <code>org.springframework.web.servlet.DispatcherServlet</code> 두 개의 Servlet이 Servlet Context에 등록되어 어떤 Servlet을 사용해야하는지 명시되어 있지 않아 발생했어요.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>로그를 확인 해보면 다음과 같이 둘 중 하나의 <code>RequestMatcher</code>를 사용하여 명시해 달라고 친절하게 알려주고 있어요.</p>
<blockquote>
<p>This method cannot decide whether these patterns are Spring MVC patterns or not. If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).</p>
</blockquote>
<p>Spring MVC 패턴을 사용하고 있다면 <code>MvcRequestMatcher</code>를 사용하고 그렇지 않은 경우에는 <code>AntPathRequestMatcher</code>를 사용하면 돼요.</p>
<p>에러가 발생된 코드를 보면 단순하게 String 배열로 <code>RequestMatcher</code>를 명시해주지 않고 사용하고 있어요.</p>
<p>에러 발생 부분: <code>.requestMatchers(WHITE_LIST).permitAll()</code></p>
<p>따라서 단순하게 <strong><em>String 배열을 인자로 넘겨주는 것이 아니라, 우리는 Spring MVC 패턴을 사용하고 있기 때문에 <code>MvcRequestMatcher</code>를 인자로 넘겨주면 돼요.</em></strong></p>
<pre><code class="language-java">@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class SecurityConfig {
    private static final String[] WHITE_LIST = {
            &quot;/helloWorld&quot;,
    };

    @Bean
    public MvcRequestMatcher.Builder mvcRequestMatcherBuilder(HandlerMappingIntrospector introspector) {
        return new MvcRequestMatcher.Builder(introspector);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .headers(header -&gt; header.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
                .authorizeHttpRequests(request -&gt; request
                        .requestMatchers(PathRequest.toH2Console()).permitAll()
                        .requestMatchers(this.createMvcRequestMatcherForWhitelist(mvc)).permitAll()
                        .anyRequest().authenticated())
                .build();
    }

    private MvcRequestMatcher[] createMvcRequestMatcherForWhitelist(MvcRequestMatcher.Builder mvc) {
        return Stream.of(WHITE_LIST).map(mvc::pattern).toArray(MvcRequestMatcher[]::new);
    }
}</code></pre>
<ol>
<li><code>HandlerMappingIntrospector</code> 빈을 주입 받고, <code>MvcRequestMatcher.Builder</code> 인자로 넘겨주어 인스턴스를 생성하여 빈으로 등록해요.</li>
<li><code>createMvcRequestMatcherForWhitelist</code> 메서드를 통해서 WHITE_LIST <code>String[]</code>을 <code>MvcRequestMathcer[]</code>로 매핑해요.</li>
<li><code>requestMatchers</code>에 <code>String[]</code>을 전달하는 것이 아니라 <code>createMvcRequestMatcherForWhitelist</code> 메서드를 통해 <code>MvcRequestMathcer[]</code>을 전달해서 <code>MvcRequestMatcher</code> 사용을 명시하여 이번 이슈를 해결할 수 있어요.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[🐛 [Spring Boot] Could not resolve org.springframework.boot:spring-boot-gradle-plugin:3.1.4. 이슈 해결]]></title>
            <link>https://velog.io/@dev_lee/Spring-Boot-Could-not-resolve-org.springframework.bootspring-boot-gradle-plugin3.1.4.-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_lee/Spring-Boot-Could-not-resolve-org.springframework.bootspring-boot-gradle-plugin3.1.4.-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 16 Oct 2023 13:20:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/8fb869cd-e44c-47f1-8c7e-daff259d2173/image.jpg" alt=""></p>
<h2 id="개요">개요</h2>
<p><strong><em>Spring Boot</em></strong> <code>3.1.4</code> 버전을 사용하면서 <code>Could not resolve org.springframework.boot:spring-boot-gradle-plugin:3.1.4.</code> 에러가 발생하며 Gradle 빌드가 실패하는 이슈가 발생됐어요.</p>
<h2 id="개발-환경">개발 환경</h2>
<ul>
<li><strong>Spring Boot</strong>: <code>3.1.4</code></li>
<li><strong>Language</strong>: <code>Java 17</code></li>
<li><strong>Build Tool</strong>: <code>Gradle 8.3</code></li>
<li><strong>IDE</strong>: <code>Intellij Ultimate</code></li>
</ul>
<h2 id="에러-로그">에러 로그</h2>
<pre><code class="language-jsx">A problem occurred configuring root project &#39;spring-demo&#39;.
&gt; Could not resolve all files for configuration &#39;:classpath&#39;.
   &gt; Could not resolve org.springframework.boot:spring-boot-gradle-plugin:3.1.4.
     Required by:
         project : &gt; org.springframework.boot:org.springframework.boot.gradle.plugin:3.1.4
      &gt; No matching variant of org.springframework.boot:spring-boot-gradle-plugin:3.1.4 was found. The consumer was configured to find a library for use during runtime, compatible with Java 8, packaged as a jar, and its dependencies declared externally, as well as attribute &#39;org.gradle.plugin.api-version&#39; with value &#39;8.3&#39; but:
          - Variant &#39;apiElements&#39; capability org.springframework.boot:spring-boot-gradle-plugin:3.1.4 declares a library, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component for use during compile-time, compatible with Java 17 and the consumer needed a component for use during runtime, compatible with Java 8
              - Other compatible attribute:
                  - Doesn&#39;t say anything about org.gradle.plugin.api-version (required &#39;8.3&#39;)
          - Variant &#39;javadocElements&#39; capability org.springframework.boot:spring-boot-gradle-plugin:3.1.4 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn&#39;t say anything about its target Java version (required compatibility with Java 8)
                  - Doesn&#39;t say anything about its elements (required them packaged as a jar)
                  - Doesn&#39;t say anything about org.gradle.plugin.api-version (required &#39;8.3&#39;)
          - Variant &#39;mavenOptionalApiElements&#39; capability org.springframework.boot:spring-boot-gradle-plugin-maven-optional:3.1.4 declares a library, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component for use during compile-time, compatible with Java 17 and the consumer needed a component for use during runtime, compatible with Java 8
              - Other compatible attribute:
                  - Doesn&#39;t say anything about org.gradle.plugin.api-version (required &#39;8.3&#39;)
          - Variant &#39;mavenOptionalRuntimeElements&#39; capability org.springframework.boot:spring-boot-gradle-plugin-maven-optional:3.1.4 declares a library for use during runtime, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component, compatible with Java 17 and the consumer needed a component, compatible with Java 8
              - Other compatible attribute:
                  - Doesn&#39;t say anything about org.gradle.plugin.api-version (required &#39;8.3&#39;)
          - Variant &#39;runtimeElements&#39; capability org.springframework.boot:spring-boot-gradle-plugin:3.1.4 declares a library for use during runtime, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component, compatible with Java 17 and the consumer needed a component, compatible with Java 8
              - Other compatible attribute:
                  - Doesn&#39;t say anything about org.gradle.plugin.api-version (required &#39;8.3&#39;)
          - Variant &#39;sourcesElements&#39; capability org.springframework.boot:spring-boot-gradle-plugin:3.1.4 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn&#39;t say anything about its target Java version (required compatibility with Java 8)
                  - Doesn&#39;t say anything about its elements (required them packaged as a jar)
                  - Doesn&#39;t say anything about org.gradle.plugin.api-version (required &#39;8.3&#39;)

* Try:
&gt; Run with --stacktrace option to get the stack trace.
&gt; Run with --info or --debug option to get more log output.
&gt; Run with --scan to get full insights.
&gt; Get more help at https://help.gradle.org.</code></pre>
<h2 id="원인">원인</h2>
<p>Spring boot가 3 버전대로 올라가면서 Java 버전을 17 이상부터만 지원하게 되었어요. 이에 따라 Gradle JVM 자바 버전 설정이 17로 설정이 되어있지 않아 발생했어요.</p>
<h2 id="해결-방법">해결 방법</h2>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/5ad4cea9-65fd-4ad0-940a-b97bd012439e/image.png" alt=""></p>
<p>Gradle JVM을 17버전으로 맞추어 해결할 수 있었어요.</p>
<ol>
<li><code>Settings</code> → <code>Build, Execution, Deployment</code> → <code>Build Tools</code> → <code>Gradle</code></li>
<li><code>Gradle JVM</code>: Project SDK 또는 개별적으로 받은 Java 17 이상 버전으로 설정해주세요.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[💸 [Redis] 레디스 데이터 타입: Hashes]]></title>
            <link>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Hashes</link>
            <guid>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Hashes</guid>
            <pubDate>Thu, 05 Oct 2023 11:13:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/19e97684-7858-4fa4-9a12-3389cef2866f/image.png" alt=""></p>
<p>안녕하세요 이서에요, 이번 포스트에서는 레디스 타입인 <code>Hashes</code>에 대해 포스팅하려 해요.</p>
<h2 id="개요">개요</h2>
<p>Redis의 <code>Hashes</code> 데이터 타입은 필드-값 쌍을 저장하는 <strong><em>해시 맵 구조</em></strong>를 제공해요. 이러한 필드-값 쌍은 문자열 키를 기반으로 저장돼요. Hashes는 데이터를 구조화하고,  필드 수준에서 읽고 쓰는 작업을 효과적으로 처리할 때 유용하며, 특히 개별 필드를 업데이트하거나 조회할 때 빠른 성능을 제공해요. Redis Hashes는 사용자 프로필, 설정, 카운터, 주소 정보와 같이 다양한 데이터 구조에 적용할 수 있어요.</p>
<p>Hashes 데이터 타입이 필드 수준에서 읽고 쓰는 작업이 효과적인 이유는 첫 번째로 복잡한 데이터 구조 표현이 가능하여 데이터를 조직적으로 저장하고 관리할 수 있으며, 두 번째로는 필드 단위로 값을 수정할 수 있어, 전체 데이터를 업데이트하지 않고 필요한 특정 필드만 수정할 수 있기 때문이에요.</p>
<h2 id="명령어">명령어</h2>
<ol>
<li><code>HSET</code> key field value [field value …]: 특정 해시 맵(key)에 하나 이상의 필드(field)와 값(value)을 설정해요.</li>
</ol>
<pre><code class="language-bash">&gt; HSET h1 f1 v1
(integer) 1

&gt; HSET h1 f2 v2 f3 v3
(integer) 2</code></pre>
<ol start="2">
<li><code>HGET</code> key field: 특정 해시 맵에서 지정한 필드의 값을 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; HGET h1 f1
&quot;v1&quot;</code></pre>
<ol start="3">
<li><code>HDEL</code> key field [field …]: 특정 해시 맵에서 하나 이상의 필드를 제거해요.</li>
</ol>
<pre><code class="language-bash">&gt; HDEL h1 f1
(integer) 1

&gt; HDEL h1 f2 f3
(integer) 2

&gt; HDEL h1 f1
(integer) 0</code></pre>
<ol start="4">
<li><code>HEXISTS</code> key field: 특정 해시 맵에서 특정 필드가 존재하는지 여부를 확인할 수 있어요.</li>
</ol>
<pre><code class="language-bash">&gt; HSET h1 f1 v1 f2 v2 f3 v3
(integer) 3

&gt; HEXISTS h1 f1
(integer) 1

&gt; HEXISTS h1 f4
(integer) 0</code></pre>
<ol start="5">
<li><code>HGETALL</code> key: 특정 해시 맵의 모든 필드와 값의 쌍을 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; HGETALL h1
1) &quot;f1&quot;
2) &quot;v1&quot;
3) &quot;f2&quot;
4) &quot;v2&quot;
5) &quot;f3&quot;
6) &quot;v3&quot;</code></pre>
<ol start="6">
<li><code>HKEYS</code> key: 특정 해시 맵의 모든 필드를 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; HKEYS h1
1) &quot;f1&quot;
2) &quot;f2&quot;
3) &quot;f3&quot;</code></pre>
<ol start="7">
<li><code>HVALS</code> key: 특정 해시 맵의 모든 값을 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; HVALS h1
1) &quot;v1&quot;
2) &quot;v2&quot;
3) &quot;v3&quot;</code></pre>
<ol start="8">
<li><code>HLEN</code> key: 특정 해시 맵의 필드 수를 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; HLEN h1
(integer) 3</code></pre>
<ol start="9">
<li><code>HINCRBY</code> key field increment</li>
</ol>
<pre><code class="language-bash">&gt; HINCRBY h1 f1 1
(error) ERR hash value is not an integer

&gt; HSET h1 f4 1
(integer) 1

&gt; HINCRBY h1 f4 10
(integer) 11</code></pre>
<p>💡 <strong><em><code>HINCRBY</code>는 integer 타입에만 적용할 수 있어요.</em></strong></p>
<ol start="10">
<li>HMSET key field value [field value…]: 특정 해시맵에 여러 개의 필드와 값을 한번에 설정해요.</li>
</ol>
<pre><code class="language-bash">&gt; HMSET hm1 f1 v1 f2 v2 f3 v3
OK</code></pre>
<ol start="11">
<li><code>HMGET</code> key field [field …]: 특정 해시 맵에서 여러 개의 필드의 값을 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; HMGET h1 f1 f2 f3
1) &quot;v1&quot;
2) &quot;v2&quot;
3) &quot;v3&quot;</code></pre>
<ol start="12">
<li><code>HSETNX</code> key field value: 특정 해시 맵에 필드가 존재하지 않을 때만 값을 설정해요.</li>
</ol>
<pre><code class="language-bash">&gt; HSETNX h1 f1 vv1
(integer) 0

&gt; HSETNX h1 f4 v1
(integer) 1</code></pre>
<h2 id="마무리">마무리</h2>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/79d9e877-8d93-4648-9ffd-099dde89dd9f/image.png" alt=""></p>
<p>이번 포스트에서는 Redis Hashes 데이터 타입에 대해서 다루어 보았어요. Hashes 데이터 타입을 사용하면 구조화된 데이터 타입을 쉽고 빠르게 조작할 수 있으니 잘 익혀두어 여러 분의 애플리케이션에 적용해보시길 바라요 😆!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💸 [Redis] 레디스 데이터 타입: Sets]]></title>
            <link>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Sets</link>
            <guid>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Sets</guid>
            <pubDate>Wed, 04 Oct 2023 12:00:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/0e7d594c-1d39-4dc0-886f-cdaa509baeb8/image.png" alt=""></p>
<p>🏎️💨 안녕하세요, 이서입니다! 이번 포스트에서는 Redis 데이터 타입 중 하나인 Sets에 대해서 포스팅하려고 합니다!</p>
<h2 id="개요">개요</h2>
<p>Redis <code>Sets</code>는 중복을 허용하지 않는 고유한 값들의 컬렉션을 저장하는 데이터 타입입니다. Sets는 집합 연산(교집합, 합집합, 차집합)을 지원하여 멤버 간의 관계를 파악하거나 필요한 멤버를 추출할 때 유용해요. 태그, 관심 주제, 온라인 사용자 목록과 같은 다양한 시나리오에서 중복 제거 및 멤버십 검사와 같은 작업을 효율적으로 수행할 수 있어요.</p>
<h2 id="명령어">명령어</h2>
<ol>
<li><code>SADD</code> key member [member …]: 하나 이상의 멤버를 Sets에 추가해요.</li>
</ol>
<pre><code class="language-bash">&gt; SADD myset 1 2 3 1 1
(integer) 3</code></pre>
<ol start="2">
<li><code>SCARD</code> key: Sets의 멤버 수를 반환해요.</li>
</ol>
<p>SCARD에서 CARD는 <code>Cardinality(카디널리티)</code>를 나타내요. 카디널리티는 집합이나 컬렉션 원소의 개수 또는 크기를 의미해요.</p>
<pre><code class="language-bash">&gt; SCAD myset
(integer) 3</code></pre>
<ol start="3">
<li><code>SISMEMBER</code> key member: 특정 멤버가 Sets에 속한 멤버인지 확인할 수 있어요.</li>
</ol>
<pre><code class="language-bash">&gt; SISMEMBER myset 1
(integer) 1
&gt; SISMEMBER myset 4
(integer) 0</code></pre>
<p>특정 멤버가 Sets에 속한 멤버이면 1을 반환하고 아니라면 0을 반환해요.</p>
<ol start="4">
<li><code>SREM</code> key member [member …]: 하나 이상의 멤버를 Sets에서 제거해요.</li>
</ol>
<pre><code class="language-bash">&gt; SREM myset 1 2
(integer) 2

&gt; SREM myset 3
(integer) 1

&gt; SREM myset 4
(integer) 0</code></pre>
<p>여러 멤버를 삭제할 경우 삭제된 멤버의 개수를 반환해요. 위의 예제에서는 1과 2를 제거 시도하였고, 1과 2가 myset에 존재하여 두 개의 원소가 제거되어 2가 반환되었어요. 반면 4의 경우에는 myset에 존재하지 않기 때문에 0이 반환되었어요.</p>
<ol start="5">
<li><code>SPOP</code> key [count]: Sets에서 무작위 멤버를 하나 또는 여러 개를 pop해요.</li>
</ol>
<pre><code class="language-bash"># 위의 예시에서 myset에서 모든 요소를 제거하였기 때문에 테스트를 위해
# 다시 10개의 원소를 넣을게요.
&gt; SAD myset 1 2 3 4 5 6 7 8 9 10
(integer) 10

&gt; SPOP myset
&quot;7&quot;

&gt; SPOP myset
&quot;1&quot;

&gt; SPOP myset 3
1) &quot;6&quot;
2) &quot;10&quot;
3) &quot;9&quot;

# 5개의 멤버를 pop하여 현재 myset에 남은 멤버는 5개에요.
&gt; SCARD myset
(integer) 5</code></pre>
<ol start="6">
<li><code>SINTER</code> key [key …]: 여러 Sets 간의 교집합을 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; SADD s1 1 2 3
(integer) 3
&gt; SADD s2 2 3 4
(integer) 3
&gt; SADD s3 3 4 5
(integer) 3

&gt; SINTER s1 s2
1) &quot;2&quot;
2) &quot;3&quot;

&gt; SINTER s2 s3
1) &quot;3&quot;
2) &quot;4&quot;

&gt; SINTER s1 s2 s3
1) &quot;3&quot;</code></pre>
<p>SINTER 명령어에서의 INTER는 <code>Intersection(교집합)</code>을 나타내요.</p>
<ol start="7">
<li><code>SUNION</code> key [key …]: 여러 Sets 간의 합집합을 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; SUNION s1 s2
1) &quot;1&quot;
2) &quot;2&quot;
3) &quot;3&quot;
4) &quot;4&quot;

&gt; SUNION s2 s3
1) &quot;2&quot;
2) &quot;3&quot;
3) &quot;4&quot;
4) &quot;5&quot;

&gt; SUNION s1 s2 s3
1) &quot;1&quot;
2) &quot;2&quot;
3) &quot;3&quot;
4) &quot;4&quot;
5) &quot;5&quot;</code></pre>
<ol start="8">
<li><code>SDIFF</code> key [key …]: 여러 Sets 간의 차집합을 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; SDIFF s1 s2
1) &quot;1&quot;

&gt; SDIFF s2 s1
1) &quot;4&quot;

&gt; SDIFF s2 s3
1) &quot;2&quot;

&gt; SDIFF s3 s2
1) &quot;5&quot;

&gt; SDIFF s1 s2 s3
1) &quot;1&quot;</code></pre>
<p>SDIFF 명령어에서 DIFF는 <code>Difference(차집합)</code>을 나타내요.</p>
<ol start="9">
<li><code>SRANDMEMBER</code> key [count]: Sets에서 무작위 멤버를 하나 또는 여러 개를 반환해요. <strong><em>단 SPOP과는 다르게 요소를 제거하지 않아요.</em></strong></li>
</ol>
<pre><code class="language-bash">&gt; SRANDMEMBER s1
&quot;3&quot;

&gt; SRANDMBMER s1 2
&quot;3&quot;
&quot;2&quot;

&gt; SCARD s1
(integer) 3</code></pre>
<h2 id="마무리">마무리</h2>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/08c34627-a503-47cc-94b9-6811283ac975/image.png" alt=""></p>
<p>이번 포스팅에서는 Redis 데이터 타입의 Sets에 대해서 다루어 보았어요. 중복을 허용하지 않는 특징을 활용하여 중복 제거, 교집합, 합집합, 차집합, 태그 등을 활용할 수 있어요. 여러 분의 애플리케이션에서 Sets를 활용할 수 있는 부분을 한번 연구해보시는 것도 좋을 것 같아요. 그럼 다음에 봬요 안녕~👋</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💸 [Redis] 레디스 데이터 타입: Lists]]></title>
            <link>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Lists</link>
            <guid>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Lists</guid>
            <pubDate>Mon, 02 Oct 2023 07:57:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/895d69f1-b284-4091-aae6-4531c221c0e8/image.png" alt=""></p>
<p>🏎️💨 안녕하세요, 이서에요! 이번 포스팅에서는 Redis의 Lists 타입에 대해서 포스팅 하고자 해요.</p>
<h2 id="개요">개요</h2>
<p>Redis의 Lists 데이터 타입은 순서가 있는 데이터의 컬렉션을 저장하는 데 사용해요. 이 데이터 타입은 중복된 값을 허용하며, 양쪽 끝에서 데이터를 추가하거나 제거할 수 있는 유연한 구조를 제공해요. Lists는 큐(Queue), 스택(Stack), 메시지 대기열(Message Queue) 등의 용도로 활용할 수 있어요.</p>
<p>Lists를 사용하면 다음과 같은 작업을 수행할 수 있어요.</p>
<ol>
<li><code>메시지 큐</code>: 메시지 브로커로 활용하여 비동기 작업 및 이벤트 처리</li>
<li><code>최신 업데이트 추적</code>: 최신 활동을 추적하고 뉴스 피드, 타임라인, 활동 스트림</li>
<li><code>작업 큐</code>: 백그라운드에서** 작업을 처리하고 스케줄링하여 시스템 부하 최소화</li>
<li><code>순서가 있는 데이터 저장</code>: 주문 목록, 로그, 히스토리 등 순서가 중요한 데이터 저장</li>
</ol>
<p>Lists는 LPOP 및 RPUSH와 같은 명령어를 사용하여 데이터를 추가하거나 제거하며, 데이터가 순차적으로 관리돼요. Lists는 메모리 내에서 데이터를 저장하므로 데이터 읽기 및 쓰기가 빠르며, 여러 클라이언트 간에 데이터를 공유하고 처리할 때 유용해요. 이제 Lists 데이터 타입을 다루는 명령어에 대해서 알아보도록 할게요 🥸.</p>
<h2 id="명령어">명령어</h2>
<ol>
<li><strong><code>LPUSH</code> key value [value ...]</strong>: 키에 해당하는 리스트의 왼쪽에 값을 추가해요.</li>
</ol>
<pre><code class="language-bash">&gt; LPUSH mylist &quot;apple&quot;
(integer) 1

# result: apple

&gt; LPUSH mylist &quot;banana&quot; &quot;cherry&quot;
(integer) 3

# result: cherry banana apple</code></pre>
<ol start="2">
<li><strong><code>RPUSH</code> key value [value ...]: 키에 해당하는 리스트의 오른쪽에 값을 추가해요.</strong></li>
</ol>
<pre><code class="language-bash">&gt; RPUSH mylist &quot;date&quot; &quot;fig&quot;
(integer) 5
cherry banana apple **date fig**</code></pre>
<ol start="3">
<li><strong><code>LPOP</code> key:</strong> 키에 해당하는 리스트에서 왼쪽 끝의 요소를 제거하고 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; LPOP mylist
cherry</code></pre>
<ol start="4">
<li><strong><code>RPOP</code> key</strong>: 키에 해당하는 리스트에서 오른쪽 끝의 요소를 제거하고 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; RPOP mylist
fig</code></pre>
<ol start="5">
<li><strong><code>LINDEX</code> key index:</strong> 키에 해당하는 리스트의 인덱스 요소를 반환해요.</li>
</ol>
<pre><code class="language-bash">&gt; LINDEX mylist 2
apple</code></pre>
<ol start="6">
<li><strong><code>LRANGE</code> key start stop: 키에 해당하는 리스트의 인덱스의 범위를 반환해요.</strong></li>
</ol>
<pre><code class="language-bash">&gt; LRANGE mylist 1 3
1) banana
2) apple
3) date

&gt; LRANGE mylist 0 -1
cherry banana apple date</code></pre>
<pre><code>*인덱스 번호를 이해하기 쉽도록 그려보았어요!*

----------------------------------
|    0   |    1   |    2  |   4  |
|   -4   |   -3   |   -2  |  -1  |
| cherry | banana | apple | date |
----------------------------------</code></pre><ol start="7">
<li><strong><code>LLEN</code> key: 키에 해당하는 리스트의 요소 개수를 반환해요(리스트의 길이를 반환해요).</strong></li>
</ol>
<pre><code class="language-bash">&gt; LLEN mylist
4</code></pre>
<ol start="8">
<li><strong><code>LINSERT</code> key BEFORE|AFTER pivot value</strong>: 키에 해당하는 리스트의 pivot의 앞, 뒤에 요소를 추가해요.</li>
</ol>
<pre><code class="language-bash">&gt; LINSERT mylist BEFORE &quot;banana&quot; &quot;grape&quot;
(integer) 5

# result: cherry grape banana apple date</code></pre>
<ol start="9">
<li><strong><code>LSET</code> key index value</strong>: 키에 해당하는 리스트의 인덱스 요소 값을 변경해요.</li>
</ol>
<pre><code class="language-bash">&gt; LSET mylist 2 &quot;lemon&quot;
OK

# result: cherry grape lemon apple date</code></pre>
<pre><code class="language-bash">만약 해당하는 인덱스가 존재하지 않으면 아래와 같은 에러 메시지가 출력돼요.

LSET mylist 10 &quot;코드레이서&quot;
&gt;&gt; (error) ERR index out of range</code></pre>
<ol start="10">
<li><strong><code>LREM</code> key count value</strong>: 키에 해당하는 리스트에서 value를 count만큼 제거해요.</li>
</ol>
<pre><code class="language-bash">&gt; LREM mylist 2 &quot;apple&quot;
(integer) 1               # 한 개의 요소가 삭제되었어요.
cherry grape lemon date</code></pre>
<h2 id="활용">활용</h2>
<h3 id="스택-stack">스택: Stack</h3>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/d1db2ae8-c8a5-4173-867f-6a0579d53d87/image.png" alt=""></p>
<p><code>lpush</code>와 <code>lpop</code>을 이용하거나, <code>rpush</code>와 <code>rpop</code>을 이용하여, <code>스택(stack)</code>을 구현할 수 있어요.</p>
<h3 id="큐-queue">큐: Queue</h3>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/08aa1fea-1647-4a6f-b25e-37ff212c94e2/image.png" alt=""></p>
<p><code>lpush</code>와 <code>rpop</code>을 이용하거나, <code>rpush</code>와 <code>lpop</code>을 이용하여, <code>큐(queue)</code>를 구현할 수 있어요.</p>
<h2 id="마무리">마무리</h2>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/98d882e6-f642-434d-be72-22f13985f79f/image.png" alt=""></p>
<p>이번 포스팅에서는 Redis의 Lists 데이터 타입에 대해서 알아보았어요. Lists는 큐, 스택, 메시지 대기열 등 다양한 용도로 활용할 수 있으니, 여러분들의 애플리케이션에 활용 및 응용 방법에 대해 연구하여 적용할 수 있기를 바라요 😎.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💸 [Redis] 레디스 데이터 타입: Strings]]></title>
            <link>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Strings</link>
            <guid>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85-Strings</guid>
            <pubDate>Sun, 01 Oct 2023 08:48:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/90f14b45-53cc-4b89-9923-94969fce20fc/image.png" alt="banner"></p>
<p>🏎️💨 안녕하세요, 이서에요! 이번 포스팅에서는 레디스 데이터 타입 중 하나인 Strings 타입에 대해서 알아보고자 해요.</p>
<h2 id="개요">개요</h2>
<p><code>Strings</code> 타입은 프로그래밍에서 문자열 데이터를 저장하는 데 사용되는 데이터 형식이에요. 문자열은 <code>작은따옴표(&#39;&#39;)</code>나 <code>큰따옴표(&quot;&quot;)</code>로 묶어서 나타내며, 문자열 안에는 문자, 숫자, 공백, 특수 문자 등 모든 종류의 문자를 포함할 수 있어요. Strings 타입은 캐싱, 세션 관리, 카운터, 설정 관리 등 다양한 용도로 활용되며, 빠른 읽기 및 쓰기 성능을 제공해요. 이제 Strings 타입을 사용하는 방법에 대해서 알아볼게요.</p>
<h2 id="명령어">명령어</h2>
<p>Redis에서 주로 사용하는 명령어 10개를 소개 드릴게요. 더 많은 명령어를 알아보고 싶으시다면, 아래의 공식 문서를 참고해 주세요 🥸.</p>
<p><a href="https://redis.io/docs/data-types/strings/">Redis Strings</a></p>
<ol>
<li><p><strong><code>SET</code> key value</strong>: 주어진 키(key)에 문자열 값을 저장해요.</p>
<pre><code class="language-bash"> &gt; SET mykey &quot;Hello, Redis&quot;
 OK</code></pre>
</li>
<li><p><strong><code>GET</code> key</strong>: 주어진 키(key)에 대한 문자열 값을 반환해요.</p>
<pre><code class="language-bash"> &gt; GET mykey
 &quot;Hello, Redis&quot;</code></pre>
</li>
<li><p><strong><code>DEL</code> key [key ...]</strong>: 하나 이상의 키(key)와 연결된 값을 삭제해요.</p>
<pre><code class="language-bash"> &gt; DEL mykey
 (integer) 1</code></pre>
</li>
<li><p><strong><code>INCRBY</code> key increment</strong>: 주어진 키(key)에 저장된 숫자 값을 주어진 increment 만큼 증가시켜요.</p>
<pre><code class="language-bash"> &gt; SET mycounter 10
 OK
 result: mycounter: 10

 &gt; INCRBY mycounter 5
 (integer) 15</code></pre>
</li>
<li><p><strong><code>DECRBY</code> key decrement</strong>: 주어진 키(key)에 저장된 숫자 값을 주어진 decrement 만큼 감소시켜요.</p>
<pre><code class="language-bash"> &gt; SET mycounter 20
 OK

 &gt; DECRBY mycounter 8
 (integer) 12</code></pre>
</li>
<li><p><strong><code>APPEND</code> key value</strong>: 주어진 키(key)에 저장된 문자열 끝에 추가 문자열 값을 붙여요.</p>
<pre><code class="language-bash"> &gt; SET mystring &quot;Hello, &quot;
 OK

 &gt; APPEND mystring &quot;Redis&quot;
 (integer) 12

 result: &quot;Hello, Redis&quot;</code></pre>
</li>
<li><p><strong><code>STRLEN</code> key</strong>: 주어진 키(key)에 저장된 문자열의 길이를 반환해요.</p>
<pre><code class="language-bash"> &gt; STRLEN mystring
 (integer) 12</code></pre>
</li>
<li><p><strong><code>SETNX</code> key value</strong>: 주어진 키(key)가 존재하지 않을 때에만 문자열 값을 저장해요.</p>
<pre><code class="language-bash"> &gt; SETNX mystring &quot;New Value&quot;
 (integer) 0  // 이미 키가 존재하기 때문에 값을 할당할 수 없어요.

 &gt; SETNX newKey &quot;New Value&quot;
 (integer) 1 // 기존에 newKey가 존재하지 않기 때문에 키와 값을 할당했어요.</code></pre>
<p> <code>NX</code>의 뜻은 <strong>N</strong>ot e<strong>X</strong>ists의 약자에요. <strong><em>만약 &quot;mykey&quot; 키가 이미 존재한다면 동작하지 않아요.</em></strong></p>
</li>
<li><p><strong><code>SETEX</code> key seconds value</strong>: 주어진 키(key)에 문자열 값을 저장하고 만료 시간(초)을 설정해요.</p>
<pre><code class="language-bash"> &gt; SETEX mykey 60 &quot;코드레이서&quot;
 OK

 GET mykey
 &gt; 코드레이서

 GET mykey  // 60초 후
 &gt; (nil)</code></pre>
<p> EX는 <strong>EX</strong>pires의 약자에요. 60초 후에 &quot;mykey&quot; 키와 값이 삭제돼요.</p>
</li>
<li><p><strong><code>GETSET</code> key value</strong>: 주어진 키(key)에 저장된 이전 값을 가져오고 새로운 값을 설정해요.</p>
<pre><code class="language-bash">&gt; SET mykey &quot;Old Value&quot;
OK  // 기존에 키가 존재하지 않는다면 (nil)을 반환해요.

&gt; GETSET mykey &quot;New Value&quot;
&quot;Old Value&quot;</code></pre>
</li>
</ol>
<h2 id="활용">활용</h2>
<h3 id="otp-one-time-password">OTP: One-Time Password</h3>
<p>Redis를 사용한 OTP(One-Time Password) 구현은 보안 및 인증 시스템에서 매우 유용해요. OTP는 한 번만 사용할 수 있는 비밀번호로, 로그인 및 인증 프로세스에서 보안을 강화하는 데 사용돼요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/02b8c9f0-8836-43a8-a086-a3d5736bef91/image.png" alt="OTP: One-Time Password"></p>
<ol>
<li>사용자가 서버에 OTP 인증을 요청해요.</li>
<li>서버에서 Redis에 정해진 시간에 만료되는 <code>SETEX</code> 명령어를 사용하여 레디스에 저장해요.</li>
<li>OTP를 레디스에 저장시킨 이후 사용자에게 OTP를 전송해요.</li>
<li>사용자는 만료시간 이전에 서버에게 전달받은 OTP를 전송해요.</li>
<li>Redis에서 가져온 OTP와 사용자가 전송해온 OTP값을 비교해요.<ul>
<li>만약 시간이 만료되었다면, Redis에서 nil을 반환해요.</li>
</ul>
</li>
<li>OTP 인증 성공을 반환해요.</li>
</ol>
<h3 id="분산락-distributed-lock">분산락: Distributed Lock</h3>
<p>분산 락이란 다수의 프로세스에서 동일한 자원을 접근할 떄 동시성 문제를 해결하기 위해 사용돼요. 예를 들어 수량의 값이 100으로 존재하고 있을 때 동시에 A는 20으로 줄이고 B는 20을 높일 경우, A는 80을 기대하고 B는 120을 기대하지만, 리턴값은 100을 받아요. 따라서 이러한 동시성 문제를 해결하기 위해 Redis를 활용해 분산락을 구현할 수 있어요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/958e379d-1eec-42d0-a23e-4e3dfdf13fe7/image.png" alt="분산락: Distributed Lock"></p>
<ol>
<li>우선 락을 획득해요. SETNX 명령어를 통해 해당 락이 존재하지 않을 경우에만 락을 생성해요.<ul>
<li><code>SETNX lock:coke “locked”</code></li>
<li>lock:coke의 락이 생성되고 난 이후에는 다른 트랜잭션이 수량을 수정하기 전에 접근할 수 없어요.</li>
<li>다른 트랜잭션이 다시 락을 획득하기 위해 SETNX lock:coke “locked” 명령어를 작성할 경우 이미 해당 key가 존재하기 때문에 저장시킬 수 없기 때문이에요.</li>
</ul>
</li>
<li>락이 항상 영원히 지속되지 않도록 만료 시간을 설정해요.<ul>
<li><code>EXPIRE lock:coke 30</code></li>
</ul>
</li>
<li>로직을 전부 처리하고 나면 락을 반환해요. 만약 서비스가 비정상적으로 종료되었더라도 EXPIRE를 통해 만료 시간을 설정해 두었기 때문에 해당 락이 자동으로 반환돼요.<ul>
<li><code>DEL lock:coke</code></li>
</ul>
</li>
</ol>
<h2 id="마무리">마무리</h2>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/6f1b3eec-daa1-4a0a-98f2-fc6ff7ca70ba/image.png" alt=""></p>
<p>Redis의 Strings 데이터 타입과 관련된 명령어 10가지와 사용 예시를 통해 Redis의 강력한 기능을 알아보았어요. Strings는 다양한 데이터 처리 작업을 지원하며, 분산락과 OTP 같은 중요한 시나리오에서도 활용돼요. Redis를 통해 데이터를 효율적으로 저장, 검색 및 조작하는 방법을 이해하셨길 바라요😎.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💸 [Redis] 레디스 설치하기 with 도커(Docker)]]></title>
            <link>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-with-%EB%8F%84%EC%BB%A4Docker</link>
            <guid>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-with-%EB%8F%84%EC%BB%A4Docker</guid>
            <pubDate>Fri, 29 Sep 2023 10:38:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/ab440f53-a1b0-4406-9d72-b905e13dc732/image.png" alt="banner"></p>
<p>🏎️💨 안녕하세요, 이서입니다! 이번 포스팅에서는 <code>도커(docker)</code>를 이용하여 레디스를 설치하는 방법에 대해서 포스팅 하고자 해요.</p>
<h2 id="도커-설치">도커 설치</h2>
<p>우선 도커가 설치 되어 있어야해요. 도커 공식 홈페이지에서 다운르드하여 설치해주세요</p>
<ul>
<li>도커 공식 홈페이지: <a href="https://www.docker.com/">https://www.docker.com/</a></li>
</ul>
<p>도커가 이미 설치 되어있다면, 도커가 실행 중인지 확인해보세요.</p>
<pre><code class="language-bash">docker ps</code></pre>
<p>만약 아래와 같은 메시지가 나타난다면 아직 도커가 실행 중이지 않은 거예요. 도커를 우선 실행해주세요.</p>
<p><code>Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?</code></p>
<h2 id="redis-이미지-다운로드">Redis 이미지 다운로드</h2>
<p>도커 허브(docker hub)에서 Redis 공식 이미지를 다운로드해요. 터미널에서 아래의 명령어를 통해 redis 최신 이미지를 다운로드할 수 있어요.</p>
<pre><code class="language-bash">docker pull redis</code></pre>
<h2 id="컨테이너-생성-및-실행">컨테이너 생성 및 실행</h2>
<p><code>run</code> 명령어를 통해 컨테이너를 생성하고 실행할 수 있어요. </p>
<ul>
<li><code>-d</code>: 옵션은 컨테이너를 백그라운드에서 실행하는 옵션이에요.</li>
<li><code>—name</code>: 옵션을 통해 원하는 컨테이너 이름을 설정할 수 있어요.</li>
<li><code>-p</code>: 옵션은 컨테이너와 호스트 시스템간의 포트 매핑을 하는 옵션이에요. 예를 들어 8080:80 으로 작성한다면 8080은 호스트 시스템의 포트 번호에요. 콜론(:) 뒤에 작성한 80 포트는 컨테이너 내부에서 실행 중인 서비스 또는 응용 프로그램이 수신 대기하고 있는 포트에요. 호스트 포트를 통해 컨테이너에 접근할 때 사용하는 포트이며 컨테이너 포트는 컨테이너 내부에서 실행 중인 서비스의 포트가 80으로 수신 대기하고 있다는 의미에요.</li>
</ul>
<pre><code class="language-bash">docker run -d --name hello-redis -p 6379:6379 redis</code></pre>
<h2 id="레디스-cli-접속">레디스 CLI 접속</h2>
<p>아래의 명령어를 통해 레디스 CLI에 접속할 수 있어요.</p>
<ul>
<li><code>exec</code>: exec 명령어는 실행 중인 도커 컨테이너 내에서 새로운 프로세스를 실행시켜요. 이를 통해 컨테이너 내부에서 명령어를 실행하거나 스크립트를 실행할 수 있어요.</li>
<li><code>-i</code>: 옵션은 인터랙티브 옵션으로 이 옵션을 사용하면 컨테이너 내부와 터미널 간에 입출력을 연결해요. 이를 통해 사용자는 명령 실행 중에 상호 작용할 수 있어요.</li>
<li><code>-t</code>: 옵션은 명령 실행을 터미널과 연결하라는 것을 나타내요. 이를 통해 터미널 관련 설정이 적용되며 터미널 명령어의 출력 형식이 올바르게 표시돼요.</li>
</ul>
<pre><code class="language-bash">docker exec -it hello-redis redis-cli</code></pre>
<p>정상적으로 접속했다면, 다음과 같은 간단한 명령어를 수행해보세요.</p>
<pre><code class="language-bash">set mykey1 &quot;Hello Redis!&quot;</code></pre>
<pre><code class="language-bash">get mykey1</code></pre>
<p>위의 명령어를 수행하면 아래와 같이 출력되는 것을 확인할 수 있어요.</p>
<pre><code class="language-bash">// 출력
127.0.0.1:6379&gt; set mykey1 &quot;Hello Redis!&quot;
OK
127.0.0.1:6379&gt; get mykey1
&quot;Hello Redis!&quot;
127.0.0.1:6379&gt;</code></pre>
<h2 id="마무리">마무리</h2>
<p>간단하게 도커로 레디스를 설치하고 실행하는 방법에 대해서 알아보았어요. 이를 기점으로 더욱 레디스에 대해 학습하고 전문가로 거듭날 수 있으면 좋겠어요😆!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💸 [Redis] 레디스 소개와 특징 및 장점 그리고 실제 활용 사례]]></title>
            <link>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EC%86%8C%EA%B0%9C%EC%99%80-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%9E%A5%EC%A0%90-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8B%A4%EC%A0%9C-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80</link>
            <guid>https://velog.io/@dev_lee/Redis-%EB%A0%88%EB%94%94%EC%8A%A4-%EC%86%8C%EA%B0%9C%EC%99%80-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%9E%A5%EC%A0%90-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8B%A4%EC%A0%9C-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80</guid>
            <pubDate>Thu, 28 Sep 2023 12:23:21 GMT</pubDate>
            <description><![CDATA[<p>🏎️💨 안녕하세요, 이서입니다! 이번 포스팅에서는 Redis란 무엇이고 특징과 장점 그리고 주요 사용 사례에 대한 예시를 작성하고자 해요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/f0ce7fd9-22ba-4427-b6aa-de36c5bea159/image.png" alt="banner"></p>
<h2 id="개요">개요</h2>
<p>Redis(<code>RE</code>mote <code>DI</code>ctionary <code>S</code>erver)는 오픈 소스 기반의 고성능 키-값 저장소이며, 메모리 내 데이터 구조 저장 및 검색을 위한 데이터베이스로 사용해요. Redis는 NoSQL 데이터베이스 중 하나로, 주로 캐싱, 세션 관리, 메시지 브로커, 대기열 처리 등 다양한 용도로 활용돼요.</p>
<p>Redis는 특히 메모리 내 데이터 저장으로 빠른 응답 속도를 제공하며, 디스크에 데이터를 지속적으로 저장하여 데이터 손실을 방지할 수도 있어요. 또한 다양한 데이터 구조를 지원하며, 문자열, 리스트, 해시, 집합, 정렬 집합과 같은 데이터 타입을 다룰 수 있어 다양한 애플리케이션에서 활용 가능해요.</p>
<p>Redis는 Pub-Sub 메커니즘을 통해 메시지 브로커로도 활용되며, 클라이언트 간 메시지 전달 및 이벤트 기반 아키텍처에서 활용할 수 있어요. Redis의 고성능과 다양한 기능은 웹 애플리케이션, 게임 서버, 실시간 분석, 캐싱, 대기열 처리 등 다양한 분야에서 널리 사용되고 있어요.</p>
<p>DB-ENGINES에서 제공한 랭킹에 따르면 전체 데이터 베이스에서는 2023년 9월 기준 6위를 기록하고 있으며, 키-벨류 저장소(Key-Value Store)에서는 1위를 기록하고 있을 정도로 많은 관심을 받고 있어요!</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/0d29003e-a1a9-4b60-9a2a-f2c1424ff4b1/image.png" alt="전체순위"></p>
<p><a href="https://db-engines.com/en/ranking">DB-Engines Ranking - 전체 순위</a></p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/b90cac9a-9c6e-4291-bf69-4524251dd9e2/image.png" alt="키벨류순위"></p>
<p><a href="https://db-engines.com/en/ranking/key-value+store">DB-Engines Ranking - 키 벨류 순위</a></p>
<h2 id="특징-및-장점">특징 및 장점</h2>
<ol>
<li><strong><code>인메모리 데이터베이스</code></strong>: Redis는 데이터를 메모리에 저장하므로 빠른 응답 속도를 제공해요.</li>
<li><strong><code>다양한 데이터 타입</code></strong>: Redis는 문자열, 리스트, 해시, 집합, 정렬 집합과 같은 다양한 데이터 구조를 지원합니다. 이를 통해 다양한 데이터 처리 요구사항을 처리하기 수월해요.</li>
<li><strong><code>영속성</code></strong>: Redis는 디스크에 데이터를 주기적으로 저장하여 데이터의 지속성을 제공해요. 이를 통해 시스템 장애 시에도 데이터 손실을 방지할 수 있어요.</li>
<li><strong><code>Pub-Sub 메시징</code></strong>: Redis는 Publish-Subscribe 메커니즘을 지원하여 메시지 브로커로 사용할 수 있어요. 이를 통해 이벤트 기반 시스템을 구축하거나 메시지 전달에 활용할 수 있어요.</li>
<li><strong><code>트랜잭션 / 싱글 스레드</code></strong>: Redis는 멀티 명령어를 원자적으로 실행하는 트랜잭션을 지원해요. 이를 통해 여러 작업을 원자적으로 처리할 수 있어요. 이러한 트랜잭션 유지가 가능한 이유는 <strong><em>Redis가 싱글 스레드 기반으로 동작하기 때문</em></strong>이에요. 따라서 Redis를 잘 이해하고 사용할 경우 사이드 이펙트가 거의 없는 매우 안정적인 서비스를 구축할 수 있어요.</li>
<li><strong><code>클러스터링</code></strong>: Redis는 데이터 샤딩과 레플리케이션을 통해 고가용성 및 확장성을 제공하는 클러스터를 구성할 수 있어요.</li>
<li><strong><code>LRU 캐시 및 만료 시간</code></strong>: Redis는 데이터를 자동으로 관리하기 위해 LRU (Least Recently Used) 알고리즘과 만료 시간(Time-to-Live)을 지원해요.</li>
<li><strong><code>트러블슈팅</code></strong>: 많은 사용자들이 존재하여 비슷한 문제 해결 사례가 많으며, 커뮤니티 도움 받기 수월해요.</li>
</ol>
<h2 id="영속성persistence">영속성(Persistence)</h2>
<p>Redis는 빠른 읽기와 쓰기 성능을 제공하지만, 메모리에 저장된 데이터는 시스템이 종료되거나 재시작 및 장애가 발생되었을 때 기본적으로 데이터가 영구적으로 보존되지 않아요. 이러한 이유는 Redis가 메모리 내에 데이터를 저장하기 때문이에요. 따라서 Redis는 이러한 단점을 보완하기 위해 다음과 같은 방법들을 제공하고 있어요.</p>
<h3 id="rdbredis-database-스냅샷">RDB(Redis DataBase) 스냅샷</h3>
<p>RDB 스냅샷은 주기적으로 Redis 데이터베이스의 스냅샷을 디스크에 저장하는 방법이에요. 이러한 스냅샷은 데이터베이스의 현재 상태를 스냅샷 파일로 저장하여, 필요 시 데이터를 복구하는 데 사용해요.</p>
<h3 id="aofappend-only-file-로그">AOF(Append Only File) 로그</h3>
<p>AOF 로그는 모든 변경 사항을 로그 파일에 기록하는 방법이에요. Redis 서버가 다시 시작될 때 이 로그를 재실행하여 데이터를 복구해요. AOF 로그는 스냅샷과 함께 사용할 수 있어요.</p>
<p>위에서 제공하는 방법들 모두 사용하지 않거나 RDB와 AOF 두가지 옵션을 각각 사용하거나 두 가지 옵션을 모두 사용할 수 있어요. 사용자는 비즈니스 환경에 맞춰 조절하여 사용할 수 있어요..</p>
<h2 id="실제-활용-사례">실제 활용 사례</h2>
<ol>
<li><strong><code>캐싱(Cashing)</code></strong>: 임시 비밀번호(One-Time Password), 로그인 세션(Session), JWT(JSON Web Token), 일정 주기로 갱신해도 괜찮은 데이터, 동일한 연산에 따른 결과</li>
<li><strong><code>실시간 분석</code></strong>: 순위(Rank), 실시간 이벤트 로그 처리, 방문자 수 계산</li>
<li><strong><code>Pub/Sub 패턴</code></strong>: 실시간 채팅, 이벤트 메시징 처리</li>
<li><strong><code>큐(Queue)</code></strong>: 우선 순위 큐, 이메일 전송</li>
</ol>
<h2 id="마무리">마무리</h2>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/b92e404a-682c-46bb-be39-8a25f804e2a5/image.png" alt="end"></p>
<p>Redis는 빠른 읽기 및 쓰기 성능을 제공하는 메모리 기반 오픈 소스 데이터베이스에요. 이번 포스팅에서 Redis의 중요한 특징과 장점, 영속성 그리고 다양한 활용 사례에 대해 살펴보았어요.</p>
<p>Redis는 메모리 내 데이터 스토어로서 데이터 검색 및 조작에 우수한 성능을 제공하며, 캐싱, 세션 스토어, 대기열 관리 등 다양한 용도로 사용하며, Pub/Sub 메커니즘을 통해 실시간 이벤트 처리와 메시징 시스템으로도 활용돼요.</p>
<p>데이터의 영속성을 보장하기 위해 스냅샷과 AOF 로그를 지원하며, 데이터 손실을 최소화하고 안정성을 높일 수 있어요. Redis는 다양한 언어와 프레임워크에서 사용할 수 있는 클라이언트 라이브러리를 지원하여 개발 생산성을 향상시킬 수 있어요.</p>
<p>이러한 특징과 장점들은 Redis를 실시간 애플리케이션, 대규모 웹 서비스, 게임 서버, 실시간 분석, 위치 기반 서비스 등 다양한 환경에서 활용 가능하게 하고, 높은 성능과 다양한 활용성을 가진 강력한 데이터베이스로, 현대적인 애플리케이션 개발에 필수적인 도구 중 하나라고 생각해요. Redis를 적재적소에 활용하여 애플리케이션의 성능을 향상시킬 수 있기를 바라요😆!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[☕️ [Java] 비동기 처리: 데몬 스레드(Daemon Thread)]]></title>
            <link>https://velog.io/@dev_lee/Java-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EB%8D%B0%EB%AA%AC-%EC%8A%A4%EB%A0%88%EB%93%9CDaemon-Thread</link>
            <guid>https://velog.io/@dev_lee/Java-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EB%8D%B0%EB%AA%AC-%EC%8A%A4%EB%A0%88%EB%93%9CDaemon-Thread</guid>
            <pubDate>Wed, 27 Sep 2023 14:31:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_lee/post/9131c78c-2868-441d-84ba-42597f77cfea/image.jpg" alt=""></p>
<p>🏎️💨 안녕하세요 이서에요. 이번 포스팅에서는 데몬 스레드에 대해서 포스팅 하려 해요😆.</p>
<h2 id="개요">개요</h2>
<p>자바(Java)에서 <code>데몬 스레드(Daemon Thread)</code>는 일반 스레드와는 다른 특성을 가지는 스레드에요. 데몬 스레드는 백그라운드에서 동작하며, 주로 서비스 스레드의 보조 역할을 수행하거나 특정 작업을 주기적으로 처리하기 위한 목적으로 사용해요. 이러한 데몬 스레드는 _<strong>프로그램이 종료될 때 자동으로 종료</strong>_되며, 명시적으로 종료시키지 않아도 돼요.</p>
<h2 id="주요-특징">주요 특징</h2>
<ol>
<li><strong>백그라운드 실행</strong>: 데몬 스레드는 백그라운드에서 동작하며 주로 애플리케이션의 주요 동작에 영향을 미치지 않고 보조적인 작업을 처리해요</li>
<li><strong>자동 종료</strong>: 데몬 스레드는 주 스레드(메인 스레드)가 종료될 때 자동으로 종료돼요. 따라서 명시적으로 종료시키지 않아도 돼요.</li>
<li><strong>setDaemon 메서드</strong>: 스레드 객체를 생성한 후, <code>setDaemon(true)</code> 메서드를 호출하여 해당 스레드를 데몬 스레드로 설정할 수 있어요.</li>
</ol>
<pre><code class="language-java">Thread daemonThread = new Thread(() -&gt; {
    while (true) {
        // 주기적인 작업 수행
        // ...
    }
});
daemonThread.setDaemon(true); // 데몬 스레드로 설정
daemonThread.start(); // 스레드 시작
</code></pre>
<h2 id="테스트">테스트</h2>
<pre><code class="language-java">System.out.println(Thread.currentThread().getName() + &quot; Thread 시작&quot;);
Thread daemonThread = new Thread(() -&gt; {
    try {
        System.out.println(Thread.currentThread().getName() + &quot; Thread 시작&quot;);
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName() + &quot; Thread 종료&quot;);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}, &quot;MyThread&quot;);
daemonThread.setDaemon(true);
daemonThread.start();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + &quot; Thread 종료&quot;);</code></pre>
<pre><code class="language-java">main Thread 시작
MyThread 시작
main Thread 종료
// MyThread 종료 메시지 출력이 안되었어요.</code></pre>
<h2 id="주-사용-사례">주 사용 사례</h2>
<ul>
<li>주기적인 로그 기록</li>
<li>자동 저장 및 백업</li>
<li>네트워크 리스너 (포트를 모니터링하며 연결 요청 처리)</li>
<li>Garbage Collection 등의 백그라운드 작업</li>
</ul>
<h2 id="마무리">마무리</h2>
<p>데몬 스레드는 백그라운드에서 동작하여 주 스레드(main 스레드)를 보조하거나 특정 작업을 처리하는 유용한 개념이에요. 하지만 주의를 기울여야 할 점도 많아요. 메인 스레드가 종료되면 데몬 스레드도 함께 종료되므로 종료 조건을 명확히 설정해야 해요. 스레드 간의 안전성과 동기화 문제도 고려해야 할 거에요. 올바른 용도와 적절한 사용으로 데몬 스레드는 성능 향상과 백그라운드 작업 관리를 효율적으로 수행할 수 있어요. 데몬 스레드에 대한 개념을 잘 숙지하여 효율적이고 성능적으로 우수한 애플리케이션을 만드시길 바라요😎.</p>
<br />
<br />

<h5 id="참고자료">참고자료</h5>
<h6 id="threadgroup-java-platform-se-8-"><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadGroup.html">ThreadGroup (Java Platform SE 8 )</a></h6>
]]></description>
        </item>
        <item>
            <title><![CDATA[☕️ [Java] 비동기 처리: Thread 클래스와 Runnable 인터페이스]]></title>
            <link>https://velog.io/@dev_lee/Java-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-Thread%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-Runnable-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@dev_lee/Java-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-Thread%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-Runnable-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Mon, 25 Sep 2023 12:50:08 GMT</pubDate>
            <description><![CDATA[<p>🏎️💨 안녕하세요 이서에요. 이번 포스팅에서는 비동기 처리의 기본이 되는 자바의 Thread와 Runnable에 대해서 포스팅하고자 해요.</p>
<h2 id="개요">개요</h2>
<p><code>Thread</code>와 <code>Runnable</code>은 자바 프로그래밍에서 다중 스레드 환경을 다룰 때 중요한 개념이에요. 이 두 가지 요소는 병렬 처리 및 동시성 작업을 관리하고 제어하는 데 사용해요. 아래는 Thread와 Runnable에 대해 간략하게 정리한 내용이에요.</p>
<ol>
<li><strong>Runnable:</strong><ul>
<li><strong>정의</strong>: Runnable은 자바에서 다중 스레드를 구현하기 위한 인터페이스에요. Runnable 인터페이스를 구현한 객체는 실행 가능한 코드를 나타내며, 스레드에 의해 실행될 수 있어요.</li>
<li><strong>사용</strong>: Runnable을 구현한 객체는 스레드 생성 시에 생성자에 전달하거나 <code>Thread</code> 클래스의 <code>run</code> 메서드를 오버라이드하여 실행 가능한 코드를 정의할 수 있어요.</li>
<li><strong>장점</strong>: Runnable을 사용하면 스레드 클래스와 작업 클래스를 분리할 수 있으며, 코드 재사용성과 유연성을 향상시킬 수 있어요</li>
</ul>
</li>
<li><strong>Thread:</strong><ul>
<li><strong>정의</strong>: 스레드는 프로세스 내에서 실행되는 작은 실행 단위로, 독립적으로 실행될 수 있는 코드의 실행 흐름입니다. 하나의 프로세스에는 여러 개의 스레드가 있을 수 있으며, 각 스레드는 동시에 실행돼요.</li>
<li><strong>스레드 생성</strong>: 자바에서는 <code>java.lang.Thread</code> 클래스를 사용하여 스레드를 생성해요. 스레드를 만들 때는 일반적으로 스레드가 실행할 코드를 <code>Runnable</code> 객체로 전달해요.</li>
<li><strong>스레드 우선순위</strong>: 각 스레드는 우선순위를 가지며, 높은 우선순위를 갖는 스레드는 낮은 우선순위를 갖는 스레드보다 CPU 자원을 더 많이 할당받을 수 있어요.</li>
<li><strong>동기화와 스레드 안전성</strong>: 다중 스레드 환경에서는 여러 스레드가 공유 자원에 동시에 접근할 수 있으므로, 동기화를 통해 데이터 무결성을 유지하고 스레드 안전성을 보장해야해요.</li>
</ul>
</li>
</ol>
<h2 id="runnable">Runnable</h2>
<p>Runnable 인터페이스는 자바에서 다중 스레드를 사용하여 병렬 처리를 구현하기 위한 핵심적인 인터페이스 중 하나에요. Thread를 실행하기 위해서는 Runnable 인터페이스를 구현한 인스턴스가 필요해요. Runnable 인터페이스는 매개변수가 없는 <code>run()</code> 메서드를 구현하도록 되어 있어요. 이 메서드에는 스레드가 실행될 때 수행할 코드를 정의해요.</p>
<pre><code class="language-java">@FunctionalInterface
public interface Runnable {
    public abstarct void run();
}</code></pre>
<h2 id="thread">Thread</h2>
<p>Thread 클래스는 우리의 프로그램 내에서 단일 Thread를 실행시킬 수 있어요. JVM(Java Virtual Machine)은 하나의 어플리케이션에서 동시에 다중 스레드를 실행할 수 있기 때문에, 여러 Thread 인스턴스를 실행시킬 수 있어요.</p>
<p>Thread를 사용하기 위해서는 기본적으로 두 가지 방법이 있어요. 첫 번째로는 Thread 클래스를 상속받아 사용하는 것이고, 두 번째로는 Runnable 인터페이스를 Thread 생성자의 매개변수로 넣어주는 방법이에요. 우선 Thread 클래스를 상속받아 사용하는 방법에 대해서 알아볼게요.</p>
<h3 id="방법1-thread-클래스를-상속받아-구현">방법1. Thread 클래스를 상속받아 구현</h3>
<p>Thread는 Runnable 인터페이스를 구현하고 있어요. 따라서 run() 메서드를 오버라이드해서 사용할 수 있어요. </p>
<pre><code class="language-java">public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(&quot;MyThread is running&quot;);
    }
}</code></pre>
<p>Thread는 <code>start()</code> 메서드를 통해 우리가 오버라이드한 <code>run()</code> 메서드를 실행할 수 있어요.</p>
<pre><code class="language-java">public static void main(String[] args) {
    Thread myThread = new MyThread();
    myThread.start(); // MyThread is running
}</code></pre>
<h3 id="방법2-runnable-인터페이스를-구현하여-인자로-전달">방법2. Runnable 인터페이스를 구현하여 인자로 전달</h3>
<p>우선 우리의 클래스에서 Runnable 인터페이스를 구현해요.</p>
<pre><code class="language-java">public class MyRunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println(&quot;MyRunnableImpl is running&quot;);
    }
}</code></pre>
<p>Thread를 인스턴스화 할 때 생성자의 매개변수로 Runnable 인스턴스를 주입해요.</p>
<pre><code class="language-java">public static void main(String[] args) {
    Runnable myRunnable = new MyRunnableImpl();
    Thread myThread= new Thread(myRunnable);
    myThread.start(); // MyRunnableImpl is running
}</code></pre>
<h3 id="방법3-익명-클래스로-runnable-인터페이스를-구현하여-인자-전달">방법3. 익명 클래스로 Runnable 인터페이스를 구현하여 인자 전달</h3>
<p>Runnable 인터페이스를 구현하지 않고, 익명 클래스로 구현할 수 있어요.</p>
<pre><code class="language-java">public static void main(String[] args) {
    Thread myThread= new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(&quot;MyRunnableImpl is running&quot;);
        }
    });
    myThread.start(); // MyRunnableImpl is running
}</code></pre>
<h3 id="방법4-람다lambda로-runnable-인터페이스를-구현하여-인자로-전달">방법4. 람다(lambda)로 Runnable 인터페이스를 구현하여 인자로 전달</h3>
<p>Runnable 인터페이스는 <code>함수형 인터페이스(Functional Interface)</code>이므로 <code>람다(lambda)</code>를 통해 구현할 수도 있어요.</p>
<pre><code class="language-java">public static void main(String[] args) {
    Thread myThread= new Thread(() -&gt; {
        @Override
        public void run() {
            System.out.println(&quot;MyRunnableImpl is running&quot;);
        }
    });
    myThread.start(); // MyRunnableImpl is running
}</code></pre>
<h2 id="priority-우선순위">Priority (우선순위)</h2>
<p>모든 Thread는 <code>priority(우선순위)</code>를 가지며, priority가 높을 수록 우선 순위가 낮은 Thread 보다 더 많은 리소스를 사용하기 위해 시도하며, 상대적으로 낮은 우선순위를 가진 스레드는 CPU 자원을 적게 얻으려고 시도해요. </p>
<p>priority의 <strong><em>기본값은 부모 Thread의 priority</em></strong>이며, <strong><em>1부터 10까지</em></strong>의 priority를 가질 수 있어요. main Thread의 경우 기본적으로 5의 priority를 가지고 있어 main Thread에서 priority를 설정하지 않고 Thread를 생성한다면 기본적으로 5의 priority를 가져요. priority는 <code>setPriority()</code> 메서드를 통해 설정할 수 있어요.</p>
<pre><code class="language-java">import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class ThreadTest {
    @Test // 테스트 통과
    public void mainThreadPriorityIs5() {
        // main Thread의 priority 테스트
        assertThat(Thread.currentThread().getPriority()).isEqualTo(5);
    }

    @Test // 테스트 통과
    public void defaultPriorityTest() {
        // 부모 Thread를 7로 설정한 이후 자식 Thread의 priority 확인 테스트
        Thread parentThread = new Thread(() -&gt; {
            Thread childThread = new Thread();
            // 부모 Thread의 우선순위를 받아 7로 설정돼요.
            assertThat(childThread.getPriority()).isEqualTo(7);
        });
        parentThread.setPriority(7); // 부모 우선순위를 7로 변경했어요.
        parentThread.start();
    }
}</code></pre>
<p>Thread 클래스에서는 상수로 priority 값들을 제공하고 있어요. 필요에 따라 활용할 수 있어요.</p>
<pre><code class="language-java">/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;</code></pre>
<p>만약 priority가 1이하 이거나 10을 초과한다면 <code>IllegalArgumentException</code>이 발생해요. 아래의 테스트 코드는 최소 priority인 1 이하이고, 최대 priority인 10을 초과하여 <code>IllegalArgumentException</code>이 발생되어 테스트 코드를 통과해요.</p>
<pre><code class="language-java">import org.junit.Test
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

public class ThreadTest {
    @Test
    public void priorityTest() {
        Thread thread = new Thread();
        assertThatIllegalArgumentException().isThrownBy(() -&gt; thread.setPriority(-1));
        assertThatIllegalArgumentException().isThrownBy(() -&gt; thread.setPriority(11));
    }
}</code></pre>
<p>스레드 우선순위는 다중 스레드 애플리케이션에서 특정 작업에 중요도를 할당하거나 특정 스레드를 특정 작업에 사용하기 위해 제어하는 데 사용될 수 있지만, 운영체제 및 하드웨어의 동작에 따라 실제로는 다소 예측하기 어려울 수 있어요. 스레드의 우선순위를 사용할 때는 주의가 필요하며, 대부분의 애플리케이션에서는 기본값인 중간 우선순위로 충분해요.</p>
<h2 id="마무리">마무리</h2>
<p>Thread와 Runnable은 자바에서 다중 스레드 프로그래밍을 효과적으로 다루기 위한 핵심 개념이에요. 스레드는 병렬 처리를 가능하게 하며 Runnable을 사용하면 실행 가능한 작업을 쉽게 정의하고 스레드에게 전달할 수 있어요. 이를 통해 자바 애플리케이션에서 동시성을 구현하고 효율적으로 다양한 작업을 처리할 수 있어요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/56450396-952f-4f87-8eb4-63ff152bf42c/image.png" alt=""></p>
<h4 id="참고자료">참고자료</h4>
<h6 id="runnable-java-platform-se-8-"><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html">Runnable (Java Platform SE 8 )</a></h6>
<h6 id="thread-java-platform-se-8-"><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html">Thread (Java Platform SE 8 )</a></h6>
]]></description>
        </item>
        <item>
            <title><![CDATA[☕️ [Java] 자바 가변 인자(Variable Arguments)와 두 가지 사용법에 대해서]]></title>
            <link>https://velog.io/@dev_lee/%EC%9E%90%EB%B0%94-%EA%B0%80%EB%B3%80-%EC%9D%B8%EC%9E%90Variable-Arguments%EC%99%80-%EB%91%90-%EA%B0%80%EC%A7%80-%EC%82%AC%EC%9A%A9%EB%B2%95%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@dev_lee/%EC%9E%90%EB%B0%94-%EA%B0%80%EB%B3%80-%EC%9D%B8%EC%9E%90Variable-Arguments%EC%99%80-%EB%91%90-%EA%B0%80%EC%A7%80-%EC%82%AC%EC%9A%A9%EB%B2%95%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Wed, 20 Sep 2023 12:35:19 GMT</pubDate>
            <description><![CDATA[<p>🏎️💨 삡삡 안녕하세요 이서입니다😇 이번 포스팅에서는 자바 가변 인자에 대해서 알아보고자 해요. RABOJA🥸</p>
<p>Java에서 가변 인자(variable arguments)는 메서드의 매개변수로 변수의 수가 가변적인 경우에 사용해요. 가변 인자를 사용하면 동일한 메서드를 다양한 개수의 인자로 호출할 수 있으며, 가독성과 코드 유지보수를 향상시킬 수 있어요. Java 5부터 이 기능이 도입되었으며, 가변 인자는 <code>...</code> (세 개의 점)으로 표시해요.</p>
<h3 id="가변-인자-특징">가변 인자 특징</h3>
<ol>
<li>매개변수로 배열을 사용하는 대신 가변 인자를 사용하여 메서드를 호출할 수 있어요.</li>
<li>가변 인자는 메서드 내에서 배열로 처리해요.</li>
<li>메서드의 선언에서 <em>가변 인자는 항상 마지막 매개변수여야 해요.</em></li>
<li>가변 인자를 사용한 메서드는 같은 타입의 인자를 여러 개 받을 수 있어요.</li>
</ol>
<h3 id="가변-인자의-사용법"><strong>가변 인자의 사용법</strong></h3>
<ol>
<li>메서드 선언 시 가변 인자를 사용할 매개변수 타입 뒤에 &quot;...&quot;을 추가해요.</li>
<li>가변 인자로 전달되는 값은 배열로 처리되므로, 메서드 내부에서 배열과 유사하게 다룰 수 있어요.</li>
</ol>
<pre><code class="language-java">public class VariableArgumentsExample {

    // 가변 인자를 사용한 메서드
    public static int sum(int... numbers) {
        int total = 0;
        for (int num : numbers) {
            total += num;
        }
        return total;
    }

    public static void main(String[] args) {
        // 다양한 개수의 인자로 메서드 호출
        int sum1 = sum(1, 2, 3);          // 6
        int sum2 = sum(10, 20, 30, 40);  // 100

        System.out.println(&quot;sum1: &quot; + sum1);
        System.out.println(&quot;sum2: &quot; + sum2);
    }
}</code></pre>
<p>위의 코드에서 <strong><code>sum</code></strong> 메서드는 가변 인자를 사용하여 다양한 개수의 정수를 더할 수 있어요. <strong>sum</strong> 메서드를 호출할 때 인자의 개수가 가변적이므로, 1개의 인자로 호출하든 4개의 인자로 호출하든 모두 동작해요.</p>
<p>가변 인자의 타입은 배열이에요. 따라서 <code>int…</code> 로 가변 인자 타입을 사용했다면, 해당 파라미터의 타입은 메서드 내에서 <code>int[]</code>이 돼요.</p>
<pre><code class="language-java">public void test(int... ints) {
    System.out.println(ints instanceof int[]); // ture
}</code></pre>
<h3 id="가변-인자를-전달하는-또다른-방법">가변 인자를 전달하는 또다른 방법</h3>
<p>우리는 보통 가변 인자를 사용하면 쉼표를 통해 여러 인자를 작성하는 방법만 알고 있는 경우가 많아요. 하지만 <em>가변 인자를 넘겨줄 때 사용 방법이 한 가지 더 있다는 점 아시나요?</em></p>
<p>가변 인자는 메서드 내부에서 배열로 처리한다는 것을 기억하시나요? 따라서 가변인자는 쉼표(,)를 통해 여러 인자를 작성할 수 있는 것 뿐만 아니라, 단 하나의 배열 인자를 받을 수도 있어요. <em>가변 인자에 배열을 넘기게 되면, 쉼표를 통해 여러 인자를 전달할 수는 없어요.</em></p>
<pre><code class="language-java">int[] ints = {1, 2, 3};
int sum3 = sum(ints); // 배열을 받을 수 있어요.
int sum4 = sum(new int[]{1, 2, 3});

// 아래의 코드는 컴파일 에러가 발생해요.
int sumError = sum(ints, 1, 2, 3); // Compile Error!!</code></pre>
<p>가변 인자를 사용하면 메서드 호출 시 편의성을 높일 수 있으며, 다양한 개수의 인자를 처리하는 데 유용해요😆</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/ba51bf05-a2d2-4e93-bbaa-ca6412a9a82b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧪 [Java] Collection toArray 와 Stream toArray 사용법 및 성능 비교]]></title>
            <link>https://velog.io/@dev_lee/Collection-toArray-%EC%99%80-Stream-toArray-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@dev_lee/Collection-toArray-%EC%99%80-Stream-toArray-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Mon, 18 Sep 2023 14:28:50 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 이서입니다. <code>Collection</code>의 <code>toArray</code> 메서드와 <code>Stream</code>의 <code>toArray</code> 메서드의 차이에 대해서 알아보고자 해요.🧐</p>
<p>Collection 인터페이스는 toArray 메서드를 구현하도록 하고 있으며, Stream 인터페이스 또한 toArray 메서드를 구현하도록 하고 있어요.</p>
<h3 id="collection의-toarray-메서드">Collection의 toArray 메서드</h3>
<p>Collection 인터페이스는 배열로 변환하는데 사용할 수 있는 toArray 메서드를 제공해요. </p>
<ol>
<li><p><strong><code>Object[] toArray()</code></strong>: 컬렉션의 요소를 <strong>Object</strong> 타입의 배열로 반환해요.</p>
</li>
<li><p><strong><code>&lt;T&gt; T[] toArray(T[] a)</code></strong>: 기본적으로 매개변수로 들어온 a배열에 Collection에 들어있는 요소를 담아서 a객체를 반환해줘요. 하지만 매개변수로 들어온 <em>a배열의 길이가 Collection에 들어있는 요소의 개수(size)보다 작다면 새로운 배열에 요소를 반환해요.</em></p>
<pre><code class="language-java"> List&lt;Integer&gt; list = Arrays.asList(1, 2, 3, 4, 5);
 Integer[] myIntegers1 = new Integer[0]; // list의 size보다 작아요.
 Integer[] myIntegers2 = new Integer[list.size()];

 Integer[] integers1 = list.toArray(myIntegers1);
 Integer[] integers2 = list.toArray(myIntegers2);

 System.out.println(myIntegers1 == integers1); // false
 System.out.println(myIntegers2 == integers2); // true</code></pre>
</li>
</ol>
<p>Collection의 toArray는 내부적으로 <code>System.arraycopy</code>를 사용해요. System.arraycopy는 native 코드를 사용하여 성능적으로 우수해요. 뿐만아니라, 내부적으로 <code>Arrays.copyOf</code> 사용되어지는데, Arrays.copyOf 또한 내부적으로  System.arraycopy를 사용해서 비슷한 성능을 가져요. 아래의 System.arraycopy와 Arrays.copyOf의 밴치마크 결과에서 확인할 수 있어요.</p>
<p><em>Baeldung에서 진행한 System.arraycopy와 Arrays.copyOf의 밴치마크 결과</em></p>
<pre><code class="language-bash">Benchmark                                          (SIZE)  Mode  Cnt        Score       Error  Units
ObjectsCopyBenchmark.arraysCopyOfBenchmark             10  avgt  100        8.535 ±     0.006  ns/op
ObjectsCopyBenchmark.arraysCopyOfBenchmark        1000000  avgt  100  2831316.981 ± 15956.082  ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark          10  avgt  100        9.278 ±     0.005  ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark     1000000  avgt  100  2826917.513 ± 15585.400  ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark          10  avgt  100        9.172 ±     0.008  ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark     1000000  avgt  100   476395.127 ±   310.189  ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark       10  avgt  100        8.952 ±     0.004  ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark  1000000  avgt  100   475088.291 ±   726.416  ns/op</code></pre>
<p><a href="https://www.baeldung.com/java-system-arraycopy-arrays-copyof-performance#4-results">Performance of System.arraycopy() vs. Arrays.copyOf() | Baeldung</a></p>
<p>🚨 <em>종종 다른 블로그 글에서 Arrays.copyOf의 성능이 System.arraycopy보다 우수하다고(많게는 2배 이상) 작성해 놓은 글이 보이는데 이는 틀린 내용이에요! Arrays.copyOf는 내부적으로 System.arraycopy를 사용하기 때문에 System.arraycopy 보다 성능적으로 크게 우수할 수 없어요.</em></p>
<h3 id="stream의-toarray-메서드">Stream의 toArray 메서드</h3>
<p><strong>Stream</strong> 인터페이스는 스트림의 요소를 배열로 변환하는 <strong><code>toArray</code></strong> 메서드를 제공해요.</p>
<ol>
<li><p><code>Object[] toArray()</code>: 컬렉션의 요소를 Object 타입의 배열로 반환해요.</p>
</li>
<li><p><code>&lt;A&gt; A[] toArray(IntFunction&lt;A[]&gt; generator)</code> : generator 매개변수를 이용하여 A[] 배열을 반환해요.</p>
<pre><code class="language-java"> List&lt;Integer&gt; list = Arrays.asList(1, 2, 3, 4, 5);
 Integer[] integers = list.stream().toArray(Integer[]::new);</code></pre>
</li>
</ol>
<p><code>list.stream().toArray(Integer[]::new)</code>를 수행할 때 해당 메서드는 컬렉션 요소의 개수를 알 수 없어요. 따라서 Stream의 toArray 메서드는 컬렉션의 모든 값을 수집한 다음 배열을 새롭게 생성하여 해당 배열에 복사해요.</p>
<p>따라서 <code>Stream</code>의 <code>toArray</code> 메서드는 <code>Collection</code>의 <code>toArray</code>보다 훨씬 느리고 더 많은 메모리를 소비해요. Stream의 <code>Object[] toArray()</code> 메서드도 마찬가지예요. 아래의 코드는 Stream의 <code>Object[] toArray()</code>의 구현 코드에요.</p>
<pre><code class="language-java">// Stream의 Object[] 배열을 반환하는 toArray() 메서드 구현
@Override
public final Object[] toArray() {
    return toArray(Object[]::new);
}</code></pre>
<h2 id="결론">결론</h2>
<p>따라서 단순하게 Collection을 배열로 반환하려 한다면 Stream의 toArray를 사용하기보다, Collection의 toArray를 사용하는 것이 더 효율적이에요.</p>
<p><img src="https://velog.velcdn.com/images/dev_lee/post/0bcaf5dc-a43f-466c-9354-6cc68760cce4/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>