<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>연향동큰손</title>
    <link>https://developerwoohyeon.tistory.com/</link>
    <description>https://github.com/yangwoohyeon</description>
    <language>ko</language>
    <pubDate>Sun, 12 Jul 2026 03:31:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>연향동큰손</managingEditor>
    <image>
      <title>연향동큰손</title>
      <url>https://tistory1.daumcdn.net/tistory/6454719/attach/3c7b2248c5c54a8886777ee5c256c103</url>
      <link>https://developerwoohyeon.tistory.com</link>
    </image>
    <item>
      <title>대용량 데이터 수집을 병렬 처리로 최적화하기</title>
      <link>https://developerwoohyeon.tistory.com/265</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 고용24에서 제공하는 &quot;내일배움카드 교육&quot; 정보를 OPEN API로 호출하는 방법 대신 DB에 저장하여 조회하는 방식으로 변경하기 위해 모든 데이터를 저장을 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #111111; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;국민내일배움카드 훈련과정 API는 총 3가지로 분류된다.&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훈련과정의 총 개수는 &lt;b&gt;88366&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;b&gt;총 1시간 37분&lt;/b&gt;이 걸렸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;233&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JWjTd/btsQxHwidpH/0RjmtME99gdYZ8lBcC1LK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JWjTd/btsQxHwidpH/0RjmtME99gdYZ8lBcC1LK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JWjTd/btsQxHwidpH/0RjmtME99gdYZ8lBcC1LK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJWjTd%2FbtsQxHwidpH%2F0RjmtME99gdYZ8lBcC1LK1%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;233&quot; height=&quot;64&quot; data-origin-width=&quot;233&quot; data-origin-height=&quot;64&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;이렇게 저장된 훈련과정 ID를 사용하여 훈련과정,기관정보, 훈련일정을 검색하기 위해서는 &lt;b&gt;하나의 훈련 ID당 두번의 API호출이 필요했다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 상세정보를 저장하기 위한 총 조회 횟수는 16만번 이상이 된다.&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;88366개의 데이터를 순차적으로 돌면서 상세정보를 수집하니 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;총 5시간 45분&lt;/b&gt;&lt;/span&gt;이 걸렸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;213&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vFZKZ/btsQzjO5KxX/LqToUKNUBEEHbT7h44tm30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vFZKZ/btsQzjO5KxX/LqToUKNUBEEHbT7h44tm30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFZKZ/btsQzjO5KxX/LqToUKNUBEEHbT7h44tm30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvFZKZ%2FbtsQzjO5KxX%2FLqToUKNUBEEHbT7h44tm30%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;213&quot; height=&quot;66&quot; data-origin-width=&quot;213&quot; data-origin-height=&quot;66&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스레드 풀 기반 병렬 처리&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;br /&gt;그러나 &lt;u&gt;&lt;b&gt;스레드 풀 기반의 병렬 처리&lt;/b&gt;&lt;/u&gt;를 도입하면서 여러 작업을 동시에 실행할 수 있었고, 그 결과 데이터 수집에 소요되는 시간을 크게 단축할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;Thread Pool(스레드 풀)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스레드 풀이란 &lt;b&gt;스레드를 미리 생성&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;하고, 작업 요청이 발생할 때 마다&amp;nbsp;&lt;/span&gt;&lt;b&gt;미리 생성된 스레드로 해당 작업을 처리&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;하는 방식을 의미한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;요청이 들어오게 되면 작업큐에 넣고&amp;nbsp;&lt;/span&gt;&lt;span&gt;스레드풀은 작업큐에 들어온&lt;span&gt;&amp;nbsp;&lt;/span&gt;Task일감을 미리 생성해놓은 Tread들에게 일감을 할당하게 된다.&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;737&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buOuc2/btsQx3zDNsE/dAIJ7aIZlkx69TB4IVSzsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buOuc2/btsQx3zDNsE/dAIJ7aIZlkx69TB4IVSzsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buOuc2/btsQx3zDNsE/dAIJ7aIZlkx69TB4IVSzsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuOuc2%2FbtsQx3zDNsE%2FdAIJ7aIZlkx69TB4IVSzsk%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;625&quot; height=&quot;261&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식을 사용하게 되면 OPEN API호출과 저장 과정을 병렬처리 하기 때문에 총 소요 시간을 크게 줄일 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구현방식&lt;/b&gt;&lt;/h4&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;ThreadPoolTaskExecutor&lt;/b&gt;를 빈으로 등록해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor은 스프링에서 제공하는 스레드 풀 구현체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 스레드 풀의 기본 동작 방식을 정의할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Configuration
public class HrdExecutorConfig {

    @Bean(name = &quot;hrdExecutor&quot;)
    public ThreadPoolTaskExecutor hrdExecutor() {
        var ex = new ThreadPoolTaskExecutor();
        ex.setCorePoolSize(8); //스레드 8개 상시 대기      
        ex.setMaxPoolSize(16); //최대 16개까지 확장 가능
        ex.setQueueCapacity(2000); //작업 대기열 크기(TaskQueue에 최대 2000개의 작업 대기)
        ex.setThreadNamePrefix(&quot;hrd-&quot;); //스레드 이름
        ex.initialize(); //스레드 풀 초기화
        return ex;
    }
}&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;setCorePoolSize : 풀에서 항상 유지되는 최소 스레드 수(0으로 설정하면 작업이 없을 때는 스레드 유지 X)&lt;/li&gt;
&lt;li&gt;setMaxPoolSize : 작업이 몰려서 큐가 꽉차게 되면 최대 스레드 개수로 늘림&lt;/li&gt;
&lt;li&gt;setQueueCapacity : 작업 대기열 크기&lt;/li&gt;
&lt;li&gt;setThreadNamePrefix : 풀에서 생성된 스레드 이름&lt;/li&gt;
&lt;li&gt;initialize : 스레드 풀 초기화&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;이제 이 스레드 풀을 저장로직이 있는 Service 클래스에서 사용하여 병렬처리를 적용시키면 된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt; futures.add(CompletableFuture.runAsync(() -&amp;gt; {
                    try {
                        hrdSearchService.getCourseFull(c.getTrprId(), c.getTrprDegr(), torgId); //두개의 OPEN API를 호출하는 메서드
                    } catch (Exception ex) {
                        log.warn(&quot;harvest-full fail trprId={}, degr={}, torgId={}, msg={}&quot;,
                                c.getTrprId(), c.getTrprDegr(), torgId, ex.getMessage());
                    }
                }, hrdExecutor));&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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&gt;CompletableFuture.runAsync&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&gt; &lt;/span&gt;스레드&lt;span&gt; &lt;/span&gt;풀&lt;span&gt;(hrdExecutor)&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;함께&lt;span&gt; &lt;/span&gt;사용&lt;/b&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;/span&gt;동시에&lt;span&gt; &lt;/span&gt;실행하도록&lt;span&gt; &lt;/span&gt;구성하였다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;처리시간 비교&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;순차적 처리&lt;/b&gt; : 5시간 45분 49초&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;213&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vFZKZ/btsQzjO5KxX/LqToUKNUBEEHbT7h44tm30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vFZKZ/btsQzjO5KxX/LqToUKNUBEEHbT7h44tm30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFZKZ/btsQzjO5KxX/LqToUKNUBEEHbT7h44tm30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvFZKZ%2FbtsQzjO5KxX%2FLqToUKNUBEEHbT7h44tm30%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;213&quot; height=&quot;66&quot; data-origin-width=&quot;213&quot; data-origin-height=&quot;66&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&gt;&lt;b&gt;병렬처리 적용&lt;/b&gt; : 1시간 5분 27초&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;202&quot; data-origin-height=&quot;47&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YKHIb/btsQzO83HQA/ibp40wh864zs5YfUuVdyG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YKHIb/btsQzO83HQA/ibp40wh864zs5YfUuVdyG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YKHIb/btsQzO83HQA/ibp40wh864zs5YfUuVdyG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYKHIb%2FbtsQzO83HQA%2Fibp40wh864zs5YfUuVdyG0%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;202&quot; height=&quot;47&quot; data-origin-width=&quot;202&quot; data-origin-height=&quot;47&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;h4 data-ke-size=&quot;size20&quot;&gt;비교 결과 병렬처리를 적용했을때 순차 처리 대비 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;5.28배&lt;/b&gt;&lt;/span&gt; 정도 수집 속도가 향상된 것을 수치상으로 확인 가능했다.&lt;/h4&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;</description>
      <category>BackEnd/Spring Boot</category>
      <category>멀티스레드</category>
      <category>병렬처리</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/265</guid>
      <comments>https://developerwoohyeon.tistory.com/265#entry265comment</comments>
      <pubDate>Mon, 15 Sep 2025 16:23:21 +0900</pubDate>
    </item>
    <item>
      <title>Dockerfile이란?</title>
      <link>https://developerwoohyeon.tistory.com/264</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cthiSW/btsQbWZXND7/Fi1KxKNH7EXIHmHPT8tx21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cthiSW/btsQbWZXND7/Fi1KxKNH7EXIHmHPT8tx21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cthiSW/btsQbWZXND7/Fi1KxKNH7EXIHmHPT8tx21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcthiSW%2FbtsQbWZXND7%2FFi1KxKNH7EXIHmHPT8tx21%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;279&quot; height=&quot;131&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;131&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Dockerfile&lt;/b&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;b&gt;Dockerfile은 Docker 이미지를 만들때 사용된다.&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;DockerHub에는 mysql, nginx, node, openjdk등 다양한 사람들이 만들어 놓은 Docker 이미지가 존재한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;689&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cakTpF/btsP9GK4VQd/wBa6cfdf3Ds7h3uP9dHTF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cakTpF/btsP9GK4VQd/wBa6cfdf3Ds7h3uP9dHTF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cakTpF/btsP9GK4VQd/wBa6cfdf3Ds7h3uP9dHTF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcakTpF%2FbtsP9GK4VQd%2FwBa6cfdf3Ds7h3uP9dHTF0%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;1587&quot; height=&quot;689&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;689&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;하지만 내가 만든 프로젝트를 Docker를 이용해서 배포해야하는 경우 &lt;b&gt;내 프로젝트를 Docker 이미지로 만들어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴때 Dockerfile을 사용하면 &lt;b&gt;나만의 Docker이미지를 만들 수 있게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Dockerfile의 구성&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FROM&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Spring Boot 애플리케이션을 Docker image로 생성할때를 예로 들어보자.&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;운영체제&lt;/li&gt;
&lt;li&gt;JDK&lt;/li&gt;
&lt;li&gt;빌드툴(Gradle,Maven..)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 환경을 매번 직접 세팅하는 것은 번거롭기 때문에, Docker에서는 &lt;b&gt;베이스 이미지(Base Image)&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;예를 들어 openjdk:17-jdk-slim을 베이스 이미지로 사용하면 운영체제와 JDK가 이미 포함되어 있어, 추가적인 설치 과정 없이도 애플리케이션 실행 환경을 손쉽게 구성할 수 있다.&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;즉, FROM 지시어에 애플리케이션 실행에 필요한 베이스 이미지를 지정하기만 하면,&lt;b&gt;최소한의 설정으로도 손쉽게 Docker 이미지를 생성할 수 있게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;COPY&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COPY는 &lt;b&gt;로컬환경에서 만든 파일이나 디렉터리를 Docker 이미지 안으로 복사&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;FROM에서 실행환경(운영체제 + JDK 등)을 마련했다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COPY에서 &lt;b&gt;실행환경 위에 내 애플리케이션 소스나 빌드 결과물(JAR파일 등)을 올려놓는 과정을 실행한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ENTRYPOINT&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENTRYPOINT는 &lt;b&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;컨테이너에서 Spring Boot 빌드 결과물인 JAR 파일을 실행하려면, ENTRYPOINT에 java -jar 명령어를 지정하면 된다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RUN&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENTRYPOINT는 컨테이너를 생성한 직후에 실행시킬 명령어를 정의 한것이면, RUN은 이미지 생성 과정에서 필요한 명령어를 실행시킬 때 사용된다.&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;예를 들어 컨테이너 환경이 Ubuntu로 구성되어 있고, git이 설치되어 있기를 바란다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile에서 RUN 지시어로 git 설치가 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1756366715127&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM ubuntu
RUN apt update &amp;amp;&amp;amp; apt install -y git
ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WORKDIR&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WORKDIR은 작업 디렉터리를 작업 디렉터리를 지정하여 RUN,CMD,ENTRYPOINT,COPY,ADD와 같은 명령어를 해당 디렉터리를 기준으로 실행시키도록 해주는 지시어다.&lt;/p&gt;
&lt;pre id=&quot;code_1756367677517&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 문법
WORKDIR [작업 디렉토리로 사용할 절대 경로]
# 예시
WORKDIR /usr/src/app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EXPOSE&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXPOSE는 컨테이너 내부에서 어떤 포트에 프로그램이 실행되는 지를 문서화 하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단지 문서화의 역할만 하기 때문에 EXPOSE를 쓰나 안 쓰나 작동 방식에는 영향을 미치지 않는다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Spring Boot 애플리케이션 Docker 컨테이너에서 실행&lt;/b&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;b&gt;1. Dockerfile 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 베이스 이미지
FROM openjdk:17-jdk

# JAR 파일 복사
COPY build/libs/Redis_Practice-0.0.1-SNAPSHOT.jar app.jar

# 실행
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&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;b&gt;2. 소스코드 빌드, JAR파일 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756362952423&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew clean build&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;3. Dockerfile 기반으로 Docker 이미지 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756363130608&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build -t hello-server .&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;4. 생성한 이미지를 기반으로 Docker 컨테이너를 생성하고 실행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드로 실행하고 호스트의 8080 포트를 컨테이너의 8080포트와 연결해서 실행시켰다.&lt;/p&gt;
&lt;pre id=&quot;code_1756363434352&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d -p 8080:8080 hello-server&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6GhAw/btsP95qGFut/cojXa4izJvkApjhogJN45K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6GhAw/btsP95qGFut/cojXa4izJvkApjhogJN45K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6GhAw/btsP95qGFut/cojXa4izJvkApjhogJN45K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6GhAw%2FbtsP95qGFut%2FcojXa4izJvkApjhogJN45K%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;937&quot; height=&quot;599&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;599&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Deployment/Docker</category>
      <category>Dockerfile</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/264</guid>
      <comments>https://developerwoohyeon.tistory.com/264#entry264comment</comments>
      <pubDate>Thu, 28 Aug 2025 14:28:39 +0900</pubDate>
    </item>
    <item>
      <title>Cloudflare를 사용하여 EC2 웹서버에 HTTPS 적용하기(2)</title>
      <link>https://developerwoohyeon.tistory.com/263</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 도메인 구매&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1681&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqaL58/btsP77hAPPT/9UQa0d2SXeErEXYxMiEerK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqaL58/btsP77hAPPT/9UQa0d2SXeErEXYxMiEerK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqaL58/btsP77hAPPT/9UQa0d2SXeErEXYxMiEerK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqaL58%2FbtsP77hAPPT%2F9UQa0d2SXeErEXYxMiEerK%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;1681&quot; height=&quot;281&quot; data-origin-width=&quot;1681&quot; data-origin-height=&quot;281&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;가비아에서 테스트용으로 'woohyeon.store'를 구입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Cloudflare에 도메인 등록&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1365&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxvmQC/btsP8i4ITus/1WRArJCWD2fIgRJxPGEcP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxvmQC/btsP8i4ITus/1WRArJCWD2fIgRJxPGEcP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxvmQC/btsP8i4ITus/1WRArJCWD2fIgRJxPGEcP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxvmQC%2FbtsP8i4ITus%2F1WRArJCWD2fIgRJxPGEcP0%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;1365&quot; height=&quot;694&quot; data-origin-width=&quot;1365&quot; data-origin-height=&quot;694&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;도메인을 등록하고, 도메인에 대한 DNS관리에서 DNS 레코드를 수정해줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVwK7a/btsQachSK3d/UknXeygvnxAM0WRnlQ4XiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVwK7a/btsQachSK3d/UknXeygvnxAM0WRnlQ4XiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVwK7a/btsQachSK3d/UknXeygvnxAM0WRnlQ4XiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVwK7a%2FbtsQachSK3d%2FUknXeygvnxAM0WRnlQ4XiK%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;1264&quot; height=&quot;588&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;588&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;도메인과 EC2 서버를 연결하기 위해서 A레코드에 EC2의 Public IP 주소를 등록해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 'www.woohyeon.store'로 접속해도 'woohyeon.store'로 맵핑되도록 해주기 위해 CNAME 레코드에 이름을 www로 등록해준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;형식 : A, 이름 : 도메인, &lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot;&gt;IPv4 주소 : EC2 인스턴스의 public IP주소&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot;&gt;형식 : CNAME 이름 : www 대상 : 도메인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot;&gt;3. SSL/TLS 등록&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot;&gt;SSL/TLS란 &lt;u&gt;&lt;b&gt;브라우저와 서버 간 통신을 암호화해주는 프로토콜&lt;/b&gt;&lt;/u&gt;이고, HTTPS로 접속할때 SSL/TLS가 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&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;&lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot;&gt;브라우저에서 나의 도메인으로 접속을 하면 그 요청은 Cloudflare를 거쳐 나의 서버로 이동하게 되는데,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot;&gt;나는 SSL모드를 '가변'으로 설정하여 브라우저와 Cloudflare 사이의 데이터를 암호화 하도록 하였다.&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;661&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsRSQ3/btsP6VhUps1/sz9I1YKixgKdbHkFAXG0n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsRSQ3/btsP6VhUps1/sz9I1YKixgKdbHkFAXG0n0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsRSQ3/btsP6VhUps1/sz9I1YKixgKdbHkFAXG0n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsRSQ3%2FbtsP6VhUps1%2Fsz9I1YKixgKdbHkFAXG0n0%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;661&quot; height=&quot;171&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;171&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. Cloudflare 네임서버를 가비아에 등록&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비아는 도메인을 내 소유로 등록해주는 곳이고, 실제 트래픽을 처리해주지 않기 때문에 이 도메인의 IP나 서브 도메인 정보를 어디서 찾아라고 알려주는 네임서버(NS)를 가비아에 등록해줘야 한다.&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhQFFd/btsP8EzsnF0/upzkHUm4FW4qX4rgzeMd10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhQFFd/btsP8EzsnF0/upzkHUm4FW4qX4rgzeMd10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhQFFd/btsP8EzsnF0/upzkHUm4FW4qX4rgzeMd10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhQFFd%2FbtsP8EzsnF0%2FupzkHUm4FW4qX4rgzeMd10%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;679&quot; height=&quot;232&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;232&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FDnom/btsQaTh0qgA/GqcX5WwNiuUQrJE1kVW4I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FDnom/btsQaTh0qgA/GqcX5WwNiuUQrJE1kVW4I1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FDnom/btsQaTh0qgA/GqcX5WwNiuUQrJE1kVW4I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFDnom%2FbtsQaTh0qgA%2FGqcX5WwNiuUQrJE1kVW4I1%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;1190&quot; height=&quot;269&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;269&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. Nginx 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare에서 웹서버로 요청이 올때는 80포트로 들어오고 이 요청을 스프링서버(8080포트)로 포워딩 해주기 위해 Nginx를 사용했다.&lt;/p&gt;
&lt;pre id=&quot;code_1756280954502&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80 default_server;
    server_name woohyeon.store www.woohyeon.store;

    # API 요청은 스프링으로 프록시
    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
    }

    # 파비콘 요청 무시
    location = /favicon.ico { return 204; }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;테스트 결과&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9ha39/btsP69mGYXe/6EkpFe0Aa1m3eUuiRPhtnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9ha39/btsP69mGYXe/6EkpFe0Aa1m3eUuiRPhtnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9ha39/btsP69mGYXe/6EkpFe0Aa1m3eUuiRPhtnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9ha39%2FbtsP69mGYXe%2F6EkpFe0Aa1m3eUuiRPhtnk%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;681&quot; height=&quot;613&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Deployment/Cloudflare</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/263</guid>
      <comments>https://developerwoohyeon.tistory.com/263#entry263comment</comments>
      <pubDate>Wed, 27 Aug 2025 16:53:47 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] 캐싱을 이용한 간단한 성능 개선 테스트(Apache JMeter)</title>
      <link>https://developerwoohyeon.tistory.com/262</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xQ3XN/btsP7pgYhG8/9tvKVoIGRfzD1tKWAFBnb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xQ3XN/btsP7pgYhG8/9tvKVoIGRfzD1tKWAFBnb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xQ3XN/btsP7pgYhG8/9tvKVoIGRfzD1tKWAFBnb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxQ3XN%2FbtsP7pgYhG8%2F9tvKVoIGRfzD1tKWAFBnb1%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;275&quot; height=&quot;183&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;캐싱이란?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS5svk/btsP57A8iya/UIj4cPnkkw6umCTN1nR2ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS5svk/btsP57A8iya/UIj4cPnkkw6umCTN1nR2ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS5svk/btsP57A8iya/UIj4cPnkkw6umCTN1nR2ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS5svk%2FbtsP57A8iya%2FUIj4cPnkkw6umCTN1nR2ek%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;627&quot; height=&quot;304&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;384&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;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;이러한 캐싱 기술은 웹 서비스의 성능을 높일 수 있다는 장점이 있지만 &lt;b&gt;무작정 사용한다면 오히려 성능 저하나 오류가 발생할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;캐싱 전략 선택시 고려사항&lt;/b&gt;&lt;/h4&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;캐싱을 통한 성능비교 테스트&lt;/b&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;/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;Apache JMeter&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;Apache JMeter란 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;서버가 제공하는 성능 및 부하를 측정할 수 있는 테스트 도구이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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&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;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;Spring에서는 여러 캐시 저장소(Redis, Ehcache, Caffeine, ConcurrentMap 등)를 사용할 수 있도록 &lt;b&gt;CacheManager&lt;/b&gt; 인터페이스를 제공한다.&lt;/span&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;/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;나는 Redis로 캐싱을 구현할 예정이므로 RedisConfig 클래스를 생성해 Redis 커넥션 설정 및 스프링 캐시 추상화에서 사용하는 CacheManager 구현체를 Redis로 설정해줬다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;@Configuration
@EnableRedisRepositories
public class RedisConfig {

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

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

    @Value(&quot;${spring.data.redis.password}&quot;)
    private String redisPassword;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
        config.setPassword(redisPassword);
        return new LettuceConnectionFactory(config);
    }

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate() {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(redisConnectionFactory());

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        GenericJackson2JsonRedisSerializer serializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        return template;
    }


    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        GenericJackson2JsonRedisSerializer serializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(serializer)
                );

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;그리고 게시글 조회 메서드에 대해서 @Cacheable를 추가해 해당 키를 가진 데이터가 캐시에 존재하지 않으면 &lt;b&gt;DB에 조회하고 캐시에 저장하도록 하고, &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 캐시 메모리에 해당 키를 가진 데이터가 캐시에 존재하면&lt;b&gt; DB조회를 하지않고 캐시에 저장된 데이터를 반환 하도록 했다.&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Cacheable(value = &quot;postsCache&quot;, key = &quot;'allPosts'&quot;)
public List&amp;lt;PostTotalListResponseDto&amp;gt; getTotalPostList() {

    List&amp;lt;Post&amp;gt; posts = postRepository.findAll();

    List&amp;lt;Post&amp;gt; sortedPosts = posts.stream()
            .sorted((p1, p2) -&amp;gt; p2.getCreatedAt().compareTo(p1.getCreatedAt()))
            .toList();

    return sortedPosts.stream()
            .map(PostTotalListResponseDto::toDto)
            .toList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테스트 시나리오&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가상 사용자수 50 -&amp;gt; 100 -&amp;gt; 500 순으로 증가&lt;/li&gt;
&lt;li&gt;한명의 가상 사용자가 10초에 걸쳐 10번씩 조회  요청&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9yynd/btsP5GX7Far/QodaEo4Y21pU1bInvYG1U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9yynd/btsP5GX7Far/QodaEo4Y21pU1bInvYG1U0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9yynd/btsP5GX7Far/QodaEo4Y21pU1bInvYG1U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9yynd%2FbtsP5GX7Far%2FQodaEo4Y21pU1bInvYG1U0%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;1770&quot; height=&quot;368&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DhSzf/btsP4vC3krz/7KNkVznZF4whfYgE3WHxw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DhSzf/btsP4vC3krz/7KNkVznZF4whfYgE3WHxw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DhSzf/btsP4vC3krz/7KNkVznZF4whfYgE3WHxw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDhSzf%2FbtsP4vC3krz%2F7KNkVznZF4whfYgE3WHxw1%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;875&quot; height=&quot;511&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;511&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;요청 수 5000의 경우 성능차이&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 응답속도 : &lt;b&gt;약 209배 빨라짐&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;처리량(TPS) : &lt;b&gt;약 3.9배 증가&lt;/b&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;/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;요청수가 5000회인 경우 캐시 없이 DB에서 데이터를 조회할때는 &lt;b&gt;평균응답이 2720ms&lt;/b&gt;까지 나타났지만 Redis를 적용한 경우 5000번의 요청에서도 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;13ms&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;TPS또한 Redis를 사용한 경우 5000번의 요청에서는 &lt;b&gt;4배 가까이 증가하는 것을 확인할 수 있었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트를 통해 Redis 캐시를 사용할 경우 낮은 부하보다 &lt;b&gt;높은 부하 환경에서 성능을 극대화&lt;/b&gt; 시킬 수 있다는 것을 알게되었다.&lt;/p&gt;</description>
      <category>BackEnd/Redis</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/262</guid>
      <comments>https://developerwoohyeon.tistory.com/262#entry262comment</comments>
      <pubDate>Mon, 25 Aug 2025 17:00:31 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot + Redis를 활용한 Refresh Token 기능 구현</title>
      <link>https://developerwoohyeon.tistory.com/261</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x83KV/btsP3AEaMXD/10ku5wPRWakPVCuekDZw41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x83KV/btsP3AEaMXD/10ku5wPRWakPVCuekDZw41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x83KV/btsP3AEaMXD/10ku5wPRWakPVCuekDZw41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx83KV%2FbtsP3AEaMXD%2F10ku5wPRWakPVCuekDZw41%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;275&quot; height=&quot;183&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Access Token만 사용할때의 문제점&lt;/b&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;Access Token은 사용자 인증과 인가에 직접 사용되므로 &lt;b&gt;상대적으로 짧은 만료 기간&lt;/b&gt;(30분 ~ 1시간)을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 짧은 유효기간을 유지하는 이유는 토큰을 탈취당했을 경우 피해를 최소화 할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Access Token의 유효 기간을 길게 설정하게 되면 &lt;b&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;이렇게 Access Token의 짧은 유효기간은 보안상 좋은점도 있지만, &lt;b&gt;사용자가 자주 로그인을 해야 하는 불편이 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 짧은 유효기간의 Access Token만을 사용하게 되면 사용자가 서비스를 사용중에 갑자기 로그아웃 되거나 오류 메시지를 보게 되어 곤란한 상황이 생길 수 있다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Refresh Token의 활용&lt;/b&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;u&gt;&lt;b&gt;Refresh Token&lt;/b&gt;&lt;/u&gt;을 함께 발급하여 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 보통 7일 이상의 긴 유효기간을 가지며, Access Token이 만료되면 클라이언트에서 Refresh Token을 사용하여 새로운 Access Token을 발급 받을 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Refresh Token 구현&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1855&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5tsEC/btsP4daPDtN/y1MFMv0ME1BMkykl3iraLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5tsEC/btsP4daPDtN/y1MFMv0ME1BMkykl3iraLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5tsEC/btsP4daPDtN/y1MFMv0ME1BMkykl3iraLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5tsEC%2FbtsP4daPDtN%2Fy1MFMv0ME1BMkykl3iraLk%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;705&quot; height=&quot;379&quot; data-origin-width=&quot;1855&quot; data-origin-height=&quot;996&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구현 내용&lt;/b&gt;&lt;/h4&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;회원정보 DB에서 조회&lt;/li&gt;
&lt;li&gt;Refresh Token(TTL 7일) Redis에 저장&lt;/li&gt;
&lt;li&gt;Access Token과 Refresh Token을 클라이언트에게 반환&lt;/li&gt;
&lt;/ol&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;Refresh Token을 Redis에 저장하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Redis는 키 단위로 TTL을 설정할 수 있어, 만료된 토큰이 자동으로 삭제 되지만 RDB에 저장할 경우 만료된 토큰을 찾아 삭제하는 추가 작업이 필요함.&lt;/li&gt;
&lt;li&gt;인메모리 방식의 빠른 조회 성능&lt;/li&gt;
&lt;li&gt;휘발성 메모리에 리프레시 토큰을 저장하다가 데이터가 전부 지워져도 모든 유저가 재로그인을 하면 되고, 그렇게 크리티컬한 오류가 아니기 때문&lt;/li&gt;
&lt;/ol&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;RefreshToken&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RedisHash(value = &quot;token&quot;)
@AllArgsConstructor
@Getter
@ToString
public class RefreshToken {

    @Id
    private Long id;

    private String refreshToken;

    @TimeToLive
    private Long expiration;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;&lt;/b&gt;&lt;b&gt;TokenRepository&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 저장소에 &lt;b&gt;RefreshToken&lt;/b&gt; 객체를 쉽게 저장하고 조회할 수 있도록 지원하는 Spring Data Repository&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface TokenRepository extends CrudRepository&amp;lt;RefreshToken, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;JwtTokenProvider&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Access Token, Refresh Token 발급 및 Redis에 Refresh Token 저장&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

        private final TokenRepository tokenRepository;

        @Value(&quot;${jwt.secret}&quot;)
        private String secretKeyString;

        @Value(&quot;${jwt.expiration}&quot;)
        private long expiration;

        private Key getSigningKey() {
            return Keys.hmacShaKeyFor(secretKeyString.getBytes(StandardCharsets.UTF_8));
        }

        public String generateToken(String email, Role role) { //사용자 이메일과 Role정보를 포함한 JWT 토큰 생성

            Date now = new Date();
            Date expiry = new Date(now.getTime() + expiration);

            return Jwts.builder()
                    .setSubject(email) // sub
                    .setIssuedAt(now)
                    .setExpiration(expiry)
                    .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                    .compact();
        }



        public String getEmail(String token) { //JWT 토큰에서 이메일 정보 추출
            return Jwts.parser()
                    .setSigningKey(getSigningKey())
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
        }

        public String getRole(String token) { //JWT 토큰에서 Role 정보 추출
            return Jwts.parser()
                    .setSigningKey(getSigningKey())
                    .parseClaimsJws(token)
                    .getBody()
                    .get(&quot;role&quot;, String.class);
        }

        public boolean validateToken(String token) { //토큰 유효성 검사
            try {
                Jwts.parser()
                        .setSigningKey(getSigningKey())
                        .parseClaimsJws(token);
                return true;
            } catch (ExpiredJwtException e) {
                System.out.println(&quot;Expired JWT token: &quot; + e.getMessage());
                throw new UnauthorizedException(ErrorStatus.UNAUTHORIZED_TOKEN_EXPIRED.getMessage());
            } catch (MalformedJwtException e) {
                System.out.println(&quot;Malformed JWT token: &quot; + e.getMessage());
                throw new UnauthorizedException(ErrorStatus.UNAUTHORIZED_INVALID_TOKEN.getMessage());
            } catch (UnsupportedJwtException e) {
                System.out.println(&quot;Unsupported JWT token: &quot; + e.getMessage());
                throw new UnauthorizedException(ErrorStatus.UNAUTHORIZED_UNSUPPORTED_TOKEN.getMessage());
            } catch (IllegalArgumentException e) {
                System.out.println(&quot;Empty JWT token: &quot; + e.getMessage());
                throw new UnauthorizedException(ErrorStatus.UNAUTHORIZED_EMPTY_TOKEN.getMessage());
            }
        }


    public String generateRefreshToken(String email) {
        Date now = new Date();

        long refreshExpiration = 7 * 24 * 60 * 60 * 1000L; // 7일 밀리초 단위
        Date expiry = new Date(now.getTime() + refreshExpiration);

        return Jwts.builder()
                .setSubject(email) // 토큰 대상자 이메일 정보
                .setIssuedAt(now)
                .setExpiration(expiry)
                // 서명 키는 기존 액세스 토큰 서명키와 동일하게 사용
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }


    /**
     * 리프레시 토큰 저장
     * @param id 토큰의 고유 식별자 (예: 사용자 ID)
     * @param refreshToken 실제 리프레시 토큰 문자열
     * @param expirationSeconds 만료 시간(초)
     */
    public void saveRefreshToken(Long id, String refreshToken, Long expirationSeconds) {
        RefreshToken token = new RefreshToken(id, refreshToken, expirationSeconds);
        tokenRepository.save(token);
    }

    /**
     * 리프레시 토큰 조회
     * @param id 토큰 고유 식별자
     * @return RefreshToken 엔티티, 없으면 null 반환
     */
    public RefreshToken getRefreshToken(Long id) {
        Optional&amp;lt;RefreshToken&amp;gt; tokenOptional = tokenRepository.findById(id);
        return tokenOptional.orElse(null);
    }

    /**
     * 리프레시 토큰 삭제
     * @param id 토큰 고유 식별자
     */
    public void deleteRefreshToken(Long id) {
        tokenRepository.deleteById(id);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;MemberService&amp;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;로그인 시 Refresh Token, Access Token 생성, 반환&lt;/li&gt;
&lt;li&gt;Refresh Token은 Redis에 저장&lt;/li&gt;
&lt;li&gt;Access Token 만료 시, Refresh Token 유효성 검사를 통해 새로운 Access Token 발급&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberService {


    private final MemberRepository memberRepository;
    private final JwtTokenProvider jwtTokenProvider;
    private final TokenRepository tokenRepository;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public void signUpMember(SignupRequestDto signupRequestDto){
        if(memberRepository.findByEmail(signupRequestDto.getEmail()).isPresent()){
            throw new BaseException(ErrorStatus.BAD_REQUEST_DUPLICATE_EMAIL.getHttpStatus(),ErrorStatus.BAD_REQUEST_DUPLICATE_EMAIL.getMessage());
        }
        if(memberRepository.findByNickname(signupRequestDto.getNickname()).isPresent()){
            throw new BaseException(ErrorStatus.BAD_REQUEST_DUPLICATE_NICKNAME.getHttpStatus(),ErrorStatus.BAD_REQUEST_DUPLICATE_NICKNAME.getMessage());
        }

        String encodedPassword = passwordEncoder.encode(signupRequestDto.getPassword());

        Member member = signupRequestDto.toEntity(encodedPassword);
        member.setRole(ROLE_MEMBER);
        memberRepository.save(member);
    }

    @Transactional
    public Map&amp;lt;String, Object&amp;gt; loginMember(LoginRequestDto loginRequestDto){

        Member member = memberRepository.findByEmail(loginRequestDto.getEmail())
                .orElseThrow(()-&amp;gt;new UnauthorizedException(ErrorStatus.UNAUTHORIZED_EMAIL_OR_PASSWORD.getMessage()));

        if(!passwordEncoder.matches(loginRequestDto.getPw(),member.getPassword())){
            throw new UnauthorizedException(ErrorStatus.UNAUTHORIZED_EMAIL_OR_PASSWORD.getMessage());
        }

        String token = jwtTokenProvider.generateToken(member.getEmail(),member.getRole());
        String refreshToken = jwtTokenProvider.generateRefreshToken(member.getEmail());

        // 리프레시 토큰 저장
        jwtTokenProvider.saveRefreshToken(member.getId(), refreshToken, 7 * 24 * 60 * 60L); // 7일 만료

        Map&amp;lt;String, Object&amp;gt; response = new HashMap&amp;lt;&amp;gt;();
        response.put(&quot;token&quot;,token);
        response.put(&quot;refreshToken&quot;, refreshToken);
        response.put(&quot;role&quot;,member.getRole());

        return response;
    }

    public String reissueAccessToken(String refreshToken){
        if(!jwtTokenProvider.validateToken(refreshToken)){
            throw new BaseException(ErrorStatus.UNAUTHORIZED_REFRESH_TOKEN_EXPIRED.getHttpStatus(),ErrorStatus.UNAUTHORIZED_EMPTY_TOKEN.getMessage());
        }

        String email = jwtTokenProvider.getEmail(refreshToken);

        Member member = memberRepository.findByEmail(email)
                .orElseThrow(()-&amp;gt;new UnauthorizedException(&quot;회원 정보를 찾을 수 없습니다.&quot;));

        //Redis에서 회원 ID로 저장된 리프레시 토큰 조회
        RefreshToken storedRefreshToken = tokenRepository.findById(member.getId())
                .orElseThrow(()-&amp;gt; new UnauthorizedException(&quot;리프레시 토큰을 찾을 수 없습니다.&quot;));

        // 전달받은 리프레시 토큰과 Redis에서 조회한 토큰 일치 여부 확인
        if(!storedRefreshToken.getRefreshToken().equals(refreshToken)){
            throw new UnauthorizedException(&quot;유효하지 않은 리프레시 토큰입니다.&quot;);
        }

        // 새로운 액세스 토큰 생성하여 반환
        return jwtTokenProvider.generateToken(email,member.getRole());

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;로그인 후 Redis CLI를 통해 확인해보면 TTL이 약 7일로 잘 설정 되어있고&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;2320&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWfRIB/btsP222exyD/X7Jknc6gKpEAdi8fONAYvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWfRIB/btsP222exyD/X7Jknc6gKpEAdi8fONAYvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWfRIB/btsP222exyD/X7Jknc6gKpEAdi8fONAYvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWfRIB%2FbtsP222exyD%2FX7Jknc6gKpEAdi8fONAYvk%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;2320&quot; height=&quot;540&quot; data-origin-width=&quot;2320&quot; data-origin-height=&quot;540&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;Access Token 재발급 API에 Refresh Token을 넣어서 테스트하면 새로운 Access Token이 발급되는 것도 확인 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cONWft/btsP47Vugpb/lniHVqTuNyEvfl8gm5kdm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cONWft/btsP47Vugpb/lniHVqTuNyEvfl8gm5kdm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cONWft/btsP47Vugpb/lniHVqTuNyEvfl8gm5kdm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcONWft%2FbtsP47Vugpb%2FlniHVqTuNyEvfl8gm5kdm1%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;686&quot; height=&quot;601&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;601&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;EC2에 배포해서 테스트&lt;/b&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;Redis가 실행될 EC2 서버 한대, Spring Application이 실행될 EC2서버 한대로 테스트를 진행했다.&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서버로 접근이 가능하게 하기 위해서는 Redis 서버의 보안그룹에서 웹서버 IP에서 Redis 서버의 6379포트로 접근가능하도록 인바운드 규칙을 편집해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1763&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qDKBx/btsP9KZFTkH/eKmBAmSLukobXMQERMMcJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qDKBx/btsP9KZFTkH/eKmBAmSLukobXMQERMMcJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qDKBx/btsP9KZFTkH/eKmBAmSLukobXMQERMMcJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqDKBx%2FbtsP9KZFTkH%2FeKmBAmSLukobXMQERMMcJ0%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;1763&quot; height=&quot;445&quot; data-origin-width=&quot;1763&quot; data-origin-height=&quot;445&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I5oWu/btsP7bRLb92/GMio4ktwrNvZ64OaKCiGAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I5oWu/btsP7bRLb92/GMio4ktwrNvZ64OaKCiGAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I5oWu/btsP7bRLb92/GMio4ktwrNvZ64OaKCiGAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI5oWu%2FbtsP7bRLb92%2FGMio4ktwrNvZ64OaKCiGAk%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;1535&quot; height=&quot;287&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인을 실행하면 hash 형식으로 Redis에 키값이 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;637&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IbZBc/btsP9GXqP0D/nLr7LtxEEfInskezkFlD30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IbZBc/btsP9GXqP0D/nLr7LtxEEfInskezkFlD30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IbZBc/btsP9GXqP0D/nLr7LtxEEfInskezkFlD30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIbZBc%2FbtsP9GXqP0D%2FnLr7LtxEEfInskezkFlD30%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;698&quot; height=&quot;637&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;637&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>BackEnd/Redis</category>
      <category>REDIS</category>
      <category>refresh token</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/261</guid>
      <comments>https://developerwoohyeon.tistory.com/261#entry261comment</comments>
      <pubDate>Sun, 24 Aug 2025 15:23:17 +0900</pubDate>
    </item>
    <item>
      <title>CORS란? Spring Boot에서 CORS 정책 설정</title>
      <link>https://developerwoohyeon.tistory.com/259</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Origin이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Origin은 출처를 의미한다. URL에서 Protocol, Host, Port가 같으면 동일한 Origin임을 의미한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DiVH3/btsP2Jg02lz/3wfH3JidFMzszlmHKKDDO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DiVH3/btsP2Jg02lz/3wfH3JidFMzszlmHKKDDO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DiVH3/btsP2Jg02lz/3wfH3JidFMzszlmHKKDDO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDiVH3%2FbtsP2Jg02lz%2F3wfH3JidFMzszlmHKKDDO0%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;640&quot; height=&quot;120&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SOP(Same Origin Policy)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Origin이 다른 두 애플리케이션이 자유롭게 소통할 수 있게되면 보안상 치명적인 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS(크로스 사이트 스크립팅), CSRF(사이트 요청 위조) 공격에 취약해질 수 있다.&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;따라서 같은 Origin만 끼리만 리소스를 공유할 수 있는 정책이 필요하다. 이 정책을 SOP(Same Origin Policy)라고 부른다.&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;하지만 다른 Origin에서의 요청을 허용해줘야 하는 경우도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 프론트엔드 서버에서 백엔드 서버로 요청을 보낼 경우 이 요청을 막아버린다면, 프론트엔드와 백엔드간 통신이 불가능하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경우 Origin이 다르더라도 특정 Origin과 리소스를 공유할 수 있도록 허용해줘야한다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CORS(Cross-Origin Resource Sharing)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS는 웹 브라우저에서 다른 출처(Origin)의 리소스에 접근할 수 있도록 하는 보안 메커니즘이다.&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;브라우저는 본 요청을 보내기 전 예비 요청(Preflight Request)을 보내서 서버와 잘 통신할 수 있는지 확인하는 요청을 보낸다.&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;서버는 어떤 출처, 어떤 HTTP 메서드, 어떤 헤더를 허용할지 응답헤더로 알려준다.&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;Preflight 요청이 성공하면 Client에서 Main Request(본 요청)을 보내게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1076&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7UTN4/btsP4YKKBIX/6KuSTigvBRSfWmtp0CpWXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7UTN4/btsP4YKKBIX/6KuSTigvBRSfWmtp0CpWXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7UTN4/btsP4YKKBIX/6KuSTigvBRSfWmtp0CpWXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7UTN4%2FbtsP4YKKBIX%2F6KuSTigvBRSfWmtp0CpWXk%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;608&quot; height=&quot;639&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1076&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Boot에서 CORS 정책 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드와 백엔드가 다른 Origin에 있을 때 백엔드에서 CORS 허용 설정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CorsConfig 클래스에서 Origin, Methods 등의 설정을 해주면 CORS 에러를 해결할 수 있다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package com.example.RedisPractice.common.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
public class CorsConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowedOrigins(List.of(
                &quot;http://localhost:3000&quot;           // 프론트엔드 개발 환경
                // 운영 환경 추가
        ));
        config.setAllowedMethods(List.of(           // 허용 메서드
                &quot;GET&quot;,
                &quot;POST&quot;,
                &quot;PUT&quot;,
                &quot;DELETE&quot;,
                &quot;OPTIONS&quot;
        ));
        config.setAllowedHeaders(List.of(&quot;*&quot;)); // 모든 요청 헤더 허용
        config.setAllowCredentials(true);           // 쿠키, 인증정보 포함 여부 허용

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, config);

        return source;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;SecurityConfig에서 빈으로 등록한 CORS 정책을 적용하면 된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.cors(Customizer.withDefaults())&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;</description>
      <category>BackEnd/Spring Boot</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/259</guid>
      <comments>https://developerwoohyeon.tistory.com/259#entry259comment</comments>
      <pubDate>Fri, 22 Aug 2025 23:49:42 +0900</pubDate>
    </item>
    <item>
      <title>Redis DB서버 구성</title>
      <link>https://developerwoohyeon.tistory.com/258</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kB8bZ/btsP20CxeoO/RRfTRK3Mp4F1svJqMP3dy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kB8bZ/btsP20CxeoO/RRfTRK3Mp4F1svJqMP3dy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kB8bZ/btsP20CxeoO/RRfTRK3Mp4F1svJqMP3dy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkB8bZ%2FbtsP20CxeoO%2FRRfTRK3Mp4F1svJqMP3dy1%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;275&quot; height=&quot;183&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&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;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Replication(복제)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EYAtv/btsP2b5DM7Q/nOpBt8Bo8KPuuNHnzUqSk0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EYAtv/btsP2b5DM7Q/nOpBt8Bo8KPuuNHnzUqSk0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EYAtv/btsP2b5DM7Q/nOpBt8Bo8KPuuNHnzUqSk0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEYAtv%2FbtsP2b5DM7Q%2FnOpBt8Bo8KPuuNHnzUqSk0%2Fimg.jpg&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;502&quot; height=&quot;359&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;424&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;Master 노드는 쓰기(Write)작업을 처리하고, 여러 Slave노드가 Master 노드의 데이터를 복제하여 읽기 작업을 처리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 요청을 여러 Slave 노드에 분산하여 트래픽 분산을 통해 처리 성능 향상&lt;/li&gt;
&lt;li&gt;Master 노드에 장애가 발생해도 Slave 노드들이 데이터를 가지고 있으므로 장애 복구에 좋다.&lt;/li&gt;
&lt;li&gt;Master는 쓰기, Slave는 읽기 작업을 담당하여 Master 노드의 부하 감소&lt;/li&gt;
&lt;li&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Redis 클러스터 구성&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/shBu6/btsP4xlR5f6/M1xetmykxqeHZtXQdvXBHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/shBu6/btsP4xlR5f6/M1xetmykxqeHZtXQdvXBHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/shBu6/btsP4xlR5f6/M1xetmykxqeHZtXQdvXBHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FshBu6%2FbtsP4xlR5f6%2FM1xetmykxqeHZtXQdvXBHk%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;433&quot; height=&quot;457&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;457&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;최소 3개의 노드를 가지고 각 노드는 Master와 Slave 로 구성되어있다.&lt;/li&gt;
&lt;li&gt;데이터를 여러 노드에 분산하여 저장함으로써 고가용성과 확장성을 제공&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;하지만&amp;nbsp;Redis&amp;nbsp;클러스터는&amp;nbsp;&lt;u&gt;&lt;b&gt;샤딩&lt;/b&gt;&lt;/u&gt;을&amp;nbsp;통해&amp;nbsp;모든&amp;nbsp;키를&amp;nbsp;16,384개의&amp;nbsp;해시&amp;nbsp;슬롯&amp;nbsp;중&amp;nbsp;하나에&amp;nbsp;명확히&amp;nbsp;매핑한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬롯들이 클러스터의 Master 노드들에 구간별로 할당되어 있어, 클라이언트는 &lt;b&gt;키 이름으로부터 슬롯을 계산하고 그 슬롯을 담당하는 노드를 자동으로 인식할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NJXbG/btsP4z44K1C/kSXoQ3evdOIkvgA8V5aKc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NJXbG/btsP4z44K1C/kSXoQ3evdOIkvgA8V5aKc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NJXbG/btsP4z44K1C/kSXoQ3evdOIkvgA8V5aKc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNJXbG%2FbtsP4z44K1C%2FkSXoQ3evdOIkvgA8V5aKc0%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;467&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>BackEnd/Redis</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/258</guid>
      <comments>https://developerwoohyeon.tistory.com/258#entry258comment</comments>
      <pubDate>Fri, 22 Aug 2025 15:57:39 +0900</pubDate>
    </item>
    <item>
      <title>Redis(Remote Dictionary Server)란?</title>
      <link>https://developerwoohyeon.tistory.com/257</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dueHde/btsP0wPfxbx/RnKuYKpDyBmumgLs1t7EL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dueHde/btsP0wPfxbx/RnKuYKpDyBmumgLs1t7EL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dueHde/btsP0wPfxbx/RnKuYKpDyBmumgLs1t7EL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdueHde%2FbtsP0wPfxbx%2FRnKuYKpDyBmumgLs1t7EL1%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;275&quot; height=&quot;183&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Redis란?&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis는 고성능의 &lt;u&gt;&lt;b&gt;키-값(key-value) 저장소&lt;/b&gt;&lt;/u&gt;로, 거대한 맵(Map) 데이터 저장소형태를 가지고 데이터를 메모리에 저장하여 빠른 읽기와 쓰기를 지원하는 오픈 소스 기반의 비관계형 데이터베이스 관리 시스템이다.&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Redis의 특징&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.&amp;nbsp; 고성능&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBS(&lt;span style=&quot;color: #222426;&quot;&gt;&lt;span&gt;Relational Database Management System)는 디스크 기반의 저장 방식을 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222426;&quot;&gt;&lt;span&gt;디스크 기반의 저장 방식은 메모리 저장 방식에 비해 &lt;b&gt;데이터를 오래 보관할 수 있다는 장점&lt;/b&gt;이 있지만,&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222426;&quot;&gt;메모리에 있는 데이터를 조회하는 방식에 비해 &lt;b&gt;처리 속도가 느리다는 단점&lt;/b&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;반면 Redis는 데이터를 디스크에 쓰는 구조가 아니라, &lt;u&gt;&lt;b&gt;메모리에서 데이터를 처리하기 때문에 속도가 매우 빠르다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Redis도 필요시 디스크에 스냅샷 저장이나 AOF(Append Only File)기록을 통해 &lt;b&gt;영속성을 지원한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.&amp;nbsp; Key- Value 방식의 단순한 데이터 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Key-Value 구조의 데이터 구조를 사용하기 때문에 SQL처럼&amp;nbsp;복잡한&amp;nbsp;쿼리&amp;nbsp;없이도&amp;nbsp;데이터&amp;nbsp;조작이&amp;nbsp;가능하며,&amp;nbsp;다양한&amp;nbsp;자료구조(Hash,&amp;nbsp;List,&amp;nbsp;Set&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. Single Thread 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 모든 클라이언트 요청 처리를 단일 스레드에서 수행한다.&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;이럴경우 트랜잭션의 일관성이 보장되어 &lt;b&gt;동시성 문제가 발생하는 것을 방지할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용 예시&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.&amp;nbsp;캐싱&amp;nbsp;시스템&amp;nbsp;(Cache)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;u&gt;자주&amp;nbsp;조회되는&amp;nbsp;데이터를&amp;nbsp;메모리에&amp;nbsp;저장&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;해&amp;nbsp;원본&amp;nbsp;데이터베이스의&amp;nbsp;부하를&amp;nbsp;줄이고&amp;nbsp;빠른&amp;nbsp;응답&amp;nbsp;속도&amp;nbsp;제공&lt;br /&gt;&amp;bull; 예:&amp;nbsp;상품&amp;nbsp;정보,&amp;nbsp;사용자&amp;nbsp;프로필,&amp;nbsp;페이지&amp;nbsp;콘텐츠&amp;nbsp;등&lt;br /&gt;&amp;bull; TTL(만료시간)&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;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;2.&amp;nbsp;세션&amp;nbsp;저장소&amp;nbsp;(Session&amp;nbsp;Store)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull; &lt;b&gt;사용자&amp;nbsp;로그인&amp;nbsp;세션&amp;nbsp;데이터를&amp;nbsp;Redis에&amp;nbsp;저장하여&amp;nbsp;빠른&amp;nbsp;인증과&amp;nbsp;세션&amp;nbsp;관리&amp;nbsp;가능&lt;/b&gt;&lt;br /&gt;&amp;bull; 여러&amp;nbsp;서버가&amp;nbsp;같은&amp;nbsp;Redis에&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;bull; &lt;u&gt;&lt;b&gt;Refresh Token&lt;/b&gt;&lt;/u&gt;을 Redis에 저장하여 로그인 상태를 지속적으로 유지하고 인증 관리 가능&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.&amp;nbsp;리더보드(순위&amp;nbsp;집계)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull; Redis의&amp;nbsp;&lt;u&gt;&lt;b&gt;Sorted&amp;nbsp;Set&lt;/b&gt;&lt;/u&gt;&amp;nbsp;자료구조를&amp;nbsp;이용해&amp;nbsp;점수&amp;nbsp;기반의&amp;nbsp;순위&amp;nbsp;계산&amp;nbsp;및&amp;nbsp;조회&lt;br /&gt;&amp;bull; 예:&amp;nbsp;게임&amp;nbsp;점수&amp;nbsp;순위,&amp;nbsp;활동량&amp;nbsp;랭킹&amp;nbsp;등&lt;br /&gt;&amp;bull; 자동&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;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.&amp;nbsp;실시간&amp;nbsp;분석&amp;nbsp;및&amp;nbsp;통계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull; 실시간&amp;nbsp;트래픽&amp;nbsp;모니터링,&amp;nbsp;사용자&amp;nbsp;활동&amp;nbsp;집계,&amp;nbsp;이상&amp;nbsp;징후&amp;nbsp;감지&amp;nbsp;등&lt;br /&gt;&amp;bull; Redis의&amp;nbsp;다양한&amp;nbsp;자료구조(Hash,&amp;nbsp;Sorted&amp;nbsp;Set&amp;nbsp;등)을&amp;nbsp;활용해&amp;nbsp;빠른&amp;nbsp;집계&amp;nbsp;및&amp;nbsp;분석&amp;nbsp;가능&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;5.&amp;nbsp;메시지&amp;nbsp;큐&amp;nbsp;및&amp;nbsp;Pub/Sub&amp;nbsp;시스템&lt;/b&gt;&lt;/h4&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;bull; 실시간&amp;nbsp;메시지&amp;nbsp;전송&amp;nbsp;및&amp;nbsp;구독&amp;nbsp;기능&amp;nbsp;지원&lt;br /&gt;&amp;bull; 채팅&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;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Redis 자료구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Redis는 0~15까지 16개의 데이터베이스를 제공한다.&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;최초 접속시 default값인 0번 데이터베이스로 이동하고, select 명령어를 이용해 데이터베이스를 선택 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1755777767282&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; select 10
OK
127.0.0.1:6379[10]&amp;gt; select 0
OK&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.&amp;nbsp; String 자료구조&lt;/b&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;String 자료구조는 Redis에서 가장 기본적이고 단순한 데이터 타입이다.&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;하나의 Key에 하나의 Value를 저장하는 단일 값 저장 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키에 값을 설정할때는 SET 명령어를 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1755778395141&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; set user:email:1 yang@naver.com
OK
127.0.0.1:6379&amp;gt; keys *
1) &quot;user:email:1&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Key의 Value를 가져올때는 GET을 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1755778466666&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; get user:email:1
&quot;yang@naver.com&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;만약 Key에 대한 Value가 존재 하는데, 동일한 Key에 대해서 SET을 통해 다른 Value를 설정하면 기존에 설정한 Value를 대신해 새로운 Value가 덮어 씌워지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TTL(Time To Live)값 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Key-Value에 대해서&amp;nbsp; Key 유지(만료) 시간을 셋팅할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'EX 초단위 시간' 형식으로 지정 가능한다.&lt;/p&gt;
&lt;pre id=&quot;code_1755778834040&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; set user:email:3 yang3@naver.com ex 10
OK
127.0.0.1:6379&amp;gt; keys *
1) &quot;user:email:2&quot;
2) &quot;user:email:1&quot;
3) &quot;user:email:3&quot;
#10초 후
127.0.0.1:6379&amp;gt; keys *
1) &quot;user:email:2&quot;
2) &quot;user:email:1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;캐싱처리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 String 자료구조는 &lt;b&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;b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초에 게시글을 조회하게 되면 DB에 접근하여 게시글을 조회하고 게시글 데이터를 캐싱하기 위해 JSON 문자열로 직렬화 하여 Redis에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 같은 요청을 하게되면 Redis에 저장되어있는 데이터를 바로 반환하기 때문에 처리 속도를 높이면서 서버 부하도 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1887&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beDNs9/btsP0Z40j5n/kKuczuGkWB5N9hxypXsHyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beDNs9/btsP0Z40j5n/kKuczuGkWB5N9hxypXsHyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beDNs9/btsP0Z40j5n/kKuczuGkWB5N9hxypXsHyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeDNs9%2FbtsP0Z40j5n%2FkKuczuGkWB5N9hxypXsHyk%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;788&quot; height=&quot;428&quot; data-origin-width=&quot;1887&quot; data-origin-height=&quot;1024&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.&amp;nbsp; List 자료구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에서 List는 일반적인 프로그래밍 언어에서의 List와 다르게 &lt;b&gt;데크(deque)&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;u&gt;&lt;b&gt;양쪽 끝에서 삽입, 삭제를 수행할 수 있는 자료구조&lt;/b&gt;&lt;/u&gt;를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Redis의 List는 데크 구조를 사용하므로 데이터를 중간에 삽입하는 것은 불가능하다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMAXlZ/btsP2fzkQXk/VjGNlqjcuI0Kc7BienCefK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMAXlZ/btsP2fzkQXk/VjGNlqjcuI0Kc7BienCefK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMAXlZ/btsP2fzkQXk/VjGNlqjcuI0Kc7BienCefK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cMAXlZ/btsP2fzkQXk/VjGNlqjcuI0Kc7BienCefK/img.gif&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;456&quot; height=&quot;342&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&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;List에 데이터를 추가 및 조회하는 방법은 다음과 같다.&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;1. lpush : 데이터를 왼쪽에 삽입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755829965065&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; lpush yangwoohyeon yang1
(integer) 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;&lt;b&gt;2. rpush : 데이터를 오른쪽에 삽입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755829976097&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; rpush yangwoohyeon yang3
(integer) 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;&lt;b&gt;3. lpop : 데이터를 왼쪽에서 꺼내기&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;b&gt;4. rpop : 데이터를 오른쪽에서 꺼내기&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;b&gt;5. lrange : list 조회 (0은 시작점, -1은 list의 끝자리 의미)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755830071422&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; lrange yangwoohyeon 0 -1
1) &quot;yang2&quot;
2) &quot;yang1&quot;
3) &quot;yang3&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;b&gt;6. llen : 데이터 개수 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755830104021&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; llen yangwoohyeon
(integer) 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;&lt;b&gt;7. expire : TTL적용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755830149752&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; expire yangwoohyeon 10
(integer) 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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;List 자료구조 활용&lt;/b&gt;&lt;/h4&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Set 자료구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Set은 &lt;u&gt;&lt;b&gt;순서가 없고 중복이 없는 자료구조&lt;/b&gt;&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;주요 명령어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. sadd : 집합에 값 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755831484980&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; sadd memberlist member1
(integer) 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;&lt;b&gt;2. scard : Set맴버 개수 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755831555415&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; scard memberlist 
(integer) 2&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;3. &lt;span style=&quot;color: #ee2323;&quot;&gt;sismember&lt;/span&gt; : 특정 맴버가 Set안에 있는지 존재여부 확인(있으면 1, 없으면 0)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755831616004&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; sismember memberlist member1
(integer) 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;&lt;b&gt;4. srem : Set에서 맴버 삭제&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755831661794&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; srem memberlist member2
(integer) 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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Set 자료구조 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SNS의 게시글에 대한 '좋아요'기능을 살펴보면 한 사용자가 한 게시글에 대해서 한번만 좋아요를 누를 수 있고, 좋아요를 누른지에 대한 여부도 불러올 필요가 있다.&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;이러한 기능에 Set 자료구조가 특화되어 있다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋아요를 누를때 Set에 사용자ID를 함께 추가하고 좋아요 여부를 '&lt;b&gt;sismember&lt;/b&gt;'로 체크하여 불러올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기존에 좋아요를 누른 사용자가 좋아요를 한번 더 누르더라도 &lt;b&gt;Set은 중복을 허용하지 않기 때문에 문제될게 없다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. zset 자료구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZSET(Sorted&amp;nbsp;Set)은&amp;nbsp;각&amp;nbsp;원소에&amp;nbsp;점수(score)를&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;주요 명령어&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;1. &lt;span&gt;zadd : 맴버-점수 쌍 추가&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755835236728&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; zadd memberlist 3 member1
(integer) 1
127.0.0.1:6379&amp;gt; zadd memberlist 4 member2
(integer) 1
127.0.0.1:6379&amp;gt; zadd memberlist 1 member3
(integer) 1
127.0.0.1:6379&amp;gt; zadd memberlist 2 member4&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &lt;span&gt;zrange : Score 기준으로 오름차순 정렬&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755835299654&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; zrange memberlist 0 -1
1) &quot;member3&quot;
2) &quot;member4&quot;
3) &quot;member1&quot;
4) &quot;member2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. zrevrange : Score 기준으로 내림차순 정렬&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755835347682&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; zrevrange memberlist 0 -1
1) &quot;member2&quot;
2) &quot;member1&quot;
3) &quot;member4&quot;
4) &quot;member3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. zrem : zset 맴버 삭제&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;b&gt;5. zrank : 특정 맴버가 몇번째(index 기준) 순서인지 출력&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755835408671&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; zrank memberlist member1
(integer) 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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ZSET 자료구조 활용&lt;/b&gt;&lt;/h4&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 본 상품이나, 주식 가격과 같이 변화가 많고 임시적이며 빠른 접근이 필요한 데이터는 Redis에 저장하는 것이 성능과 운영 측면에서 더 효율적이므로 Redis 활용이 권장된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. Hash 자료구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash는 Value값이 &lt;b&gt;map 형태&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;b&gt;객체 형식의 데이터를 캐싱할때 효율적이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;name:&amp;nbsp;&quot;Laptop&quot;&lt;br /&gt;-&amp;nbsp;brand:&amp;nbsp;&quot;Apple&quot;&lt;br /&gt;-&amp;nbsp;price:&amp;nbsp;&quot;1500&quot;&lt;br /&gt;-&amp;nbsp;stock:&amp;nbsp;&quot;50&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;주요 명령어&lt;/b&gt;&lt;/h4&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;1. hset : 필드, 값 데이터 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755838530499&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; hset author:info:1 name hong email hong@naver.com age 30
(integer) 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;&lt;b&gt;2. hget : 특정값 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755838564094&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; hget author:info:1 name
&quot;hong&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. hgetall : 모든 객체값 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755838592940&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; hgetall author:info:1
1) &quot;name&quot;
2) &quot;hong&quot;
3) &quot;email&quot;
4) &quot;hong@naver.com&quot;
5) &quot;age&quot;
6) &quot;30&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;b&gt;4. &lt;span&gt;hincrby : 필드값을 정수만큼 증가&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755838694647&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; hincrby author:info:1 age 3
(integer) 33
127.0.0.1:6379&amp;gt; hgetall author:info:1
1) &quot;name&quot;
2) &quot;kim&quot;
3) &quot;email&quot;
4) &quot;hong@naver.com&quot;
5) &quot;age&quot;
6) &quot;33&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash값은 &lt;u&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;빈번하게 변경될 가능성이 있는 객체 형식의 데이터를 캐싱&lt;/b&gt;&lt;/span&gt;&lt;/u&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;만약 JSON 형식의 문자열로 캐싱된 데이터를 수정해야하는 경우,&lt;b&gt; 일일이 파싱 후 모든 값을 다시 셋팅해야하는 비효율이 존재&lt;/b&gt;하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hash는 특정 요소값만을 변경하기에 용이하기 때문에 비교적 효율적이라고 볼 수 있다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>BackEnd/Redis</category>
      <category>REDIS</category>
      <category>자료구조</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/257</guid>
      <comments>https://developerwoohyeon.tistory.com/257#entry257comment</comments>
      <pubDate>Thu, 21 Aug 2025 17:20:48 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] MSA 구조에서의 Kafka 사용</title>
      <link>https://developerwoohyeon.tistory.com/255</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MSA(&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;Microservice Architecture&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #f7f7f8; color: #374151; text-align: start;&quot;&gt;&lt;b&gt;)&lt;/b&gt;란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f7f7f8; color: #374151; text-align: start;&quot;&gt;MSA는 큰 애플리케이션을 &lt;u&gt;&lt;b&gt;여러 개의 작고 독립적인 서비스로 나누어 개발하고 운영&lt;/b&gt;&lt;/u&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: #f7f7f8; color: #374151; text-align: start;&quot;&gt;각 서비스는 독립적으로 배포와 확장이 가능하고, 명확히 분리된 API로 통신을 하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f7f7f8; color: #374151; text-align: start;&quot;&gt;MSA의 특징&lt;/span&gt;&lt;/b&gt;&lt;/h4&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;&lt;/li&gt;
&lt;li&gt;API 기반의 통신으로 서비스 간 &lt;b&gt;느슨한 결합 유지&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;서비스 마다 &lt;b&gt;서로 다른 언어나 기술 스택 사용 가능&lt;/b&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 MSA와 같은 분산환경에서 Kafka가 유용하게 사용될 수 있다.&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; Kafka 메시지 큐를 통해 이벤트 기반의 통신을 통해 서로 의존성을 줄일 수 있고&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;b&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;또한&amp;nbsp; Kafka의 토픽은 여러 개의 파티션으로 나뉘며, 각 파티션은 독립적으로 읽기/쓰기가 가능하여 &lt;b&gt;클러스터 내 다른 브로커에 I/O부하를 분산하여 처리량을 높일 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;MSA 프로젝트에 Kafka 적용해보기&lt;/b&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;a href=&quot;https://github.com/yangwoohyeon/MSA_Kafka&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/yangwoohyeon/MSA_Kafka&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1755600365617&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 - yangwoohyeon/MSA_Kafka&quot; data-og-description=&quot;Contribute to yangwoohyeon/MSA_Kafka development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/yangwoohyeon/MSA_Kafka&quot; data-og-url=&quot;https://github.com/yangwoohyeon/MSA_Kafka&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/yangwoohyeon/MSA_Kafka&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/yangwoohyeon/MSA_Kafka&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;GitHub - yangwoohyeon/MSA_Kafka&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to yangwoohyeon/MSA_Kafka 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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현 기능&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 서비스, 프로젝트에서는 Producer의 역할 (localhost:8080)&lt;/li&gt;
&lt;li&gt;회원가입에 성공하면, DB에 저장 후, 회원정보(userId, email)를 Kafka 서버로 전송&lt;/li&gt;
&lt;li&gt;Kafka 서버에서 Email Service로 회원정보 전달&lt;/li&gt;
&lt;li&gt;Email Service에서는 회원가입 축하 이메일 전송(이메일 전송 로직 구현 생략)&lt;/li&gt;
&lt;li&gt;Kafka 서버는 AWS EC2에 구현 ( Partition개수: 3개, Replication : 3개)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbAZ8c/btsPYCOIOjn/AwwvwVqu1LRZzYsBK9BWZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbAZ8c/btsPYCOIOjn/AwwvwVqu1LRZzYsBK9BWZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbAZ8c/btsPYCOIOjn/AwwvwVqu1LRZzYsBK9BWZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbAZ8c%2FbtsPYCOIOjn%2FAwwvwVqu1LRZzYsBK9BWZk%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;939&quot; height=&quot;332&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;475&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;&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;b&gt;1. 토픽 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 분산 저장 및 병렬 처리를 위해 &lt;b&gt;3개의 파티션을 확보&lt;/b&gt;하고, 데이터 안정성과 가용성을 위해 &lt;b&gt;각 파티션을 3개의 브로커에 복제&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755586521895&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bin/kafka-topics.sh \
--bootstrap-server localhost:9092 \
--create \
--topic user.signed-up \
--partitions 3 \
--replication-factor 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. DLT 토픽 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 처리 중 오류가 발생하여 정상적으로 메시지를 처리하지 못할 경우, 처리하지 못한 메시지를 별도로 분리하여 관리하고, 시스템의 안정성과 신뢰성을 높이기 위해 DLT 토픽을 생성해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1755586556137&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bin/kafka-topics.sh \
--bootstrap-server localhost:9092 \
--create \
--topic user.signed-up.dlt \
--partitions 1 \
--replication-factor 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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 회원가입 서비스 user-service 구현&lt;/b&gt;&lt;/h4&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;&amp;lt;UsersignedUpEvent&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에 회원정보를 전달할때 Key와 Value를 String 타입으로 직렬화 하도록 지정했기 때문에 회원정보를 문자열로 변환하여 전송해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Kafka로 보낼 정보를 객체로 캡슐화 하고, 객체를 JSON 형식의 String으로 변환하여 전송하기 위해 회원 정보를 저장할 UsersignedUpEvent 클래스를 생성해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1755586852473&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.userservice;


//Kafka에 넣을 메시지 객체
public class UsersignedUpEvent {

    private Long userId;

    private String email;

    private String name;

    /*비밀번호는 이메일 서버에서 필요하지 않기 때문에 포함 X*/
    public UsersignedUpEvent(Long userId, String email, String name) {
        this.userId = userId;
        this.email = email;
        this.name = name;
    }

    public Long getUserId() {
        return userId;
    }

    public String getEmail() {
        return email;
    }

    public String getName() {
        return name;
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;UserService&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입에 성공할 경우 회원 정보를 DB에 저장하고, toJsonString메서드를 이용해 &lt;b&gt;Kafka에 전송할 객체를 JSON 형식의 String으로 변환&lt;/b&gt;하여 &lt;b&gt;user.signed-up&lt;/b&gt; 토픽으로 전송한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;package com.example.userservice;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final UserRepository userRepository;
    private final KafkaTemplate&amp;lt;String,String&amp;gt; kafkaTemplate; //Kafka 메시지 발행 객체

    public UserService(UserRepository userRepository, KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate) {
        this.userRepository = userRepository;
        this.kafkaTemplate = kafkaTemplate;
    }

    public void signUp(SignUpRequestDto signUpRequestDto){
        User user = new User( //유저 생성
                signUpRequestDto.getEmail(),
                signUpRequestDto.getName(),
                signUpRequestDto.getPassword()
        );
        User savedUser = userRepository.save(user); //유저 정보 DB에 저장

        UsersignedUpEvent usersignedUpEvent = new UsersignedUpEvent(
                savedUser.getId(),
                savedUser.getEmail(),
                savedUser.getName()
        );
        this.kafkaTemplate.send(&quot;user.signed-up&quot;,toJsonString(usersignedUpEvent));
    }

    private String toJsonString(Object object){ //Kafka에 전송할 객체를 Json형식의 String 으로 변환하기 위해 사용
        ObjectMapper objectMapper = new ObjectMapper();
        try{
            String message = objectMapper.writeValueAsString(object);
            return message;
        }catch(JsonProcessingException e){
            throw new RuntimeException(&quot;Json 직렬화 실패&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. consumer 역할을 하는 email-service 구현&lt;/b&gt;&lt;/h4&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;&amp;lt;UserSignedUpEvent&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서 받은 메시지를 객체로 역직렬화 하기위한 DTO이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package com.example.email_service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

//Kafka에서 받은 이벤트 메시지를 JSON -&amp;gt; 객체로 역질렬화하는 DTO
public class UserSignedUpEvent {
    private Long userId;
    private String email;
    private String name;

    public UserSignedUpEvent(){ //역직렬화에 필요

    }

    public UserSignedUpEvent(Long userId, String email, String name) {
        this.userId = userId;
        this.email = email;
        this.name = name;
    }

    public static UserSignedUpEvent fromJson(String json){ //수신한 JSON 문자열을 객체로 변환
        try{
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.readValue(json, UserSignedUpEvent.class);
        }catch(JsonProcessingException e){
            throw new RuntimeException(&quot;JSON 파싱 실패&quot;);
        }
    }

    public Long getUserId() {
        return userId;
    }

    public String getEmail() {
        return email;
    }

    public String getName() {
        return name;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;UserSignedUpEventConsumer&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user.signed-up 토픽을 구독하여 메시지를 수신할 수 있도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 처리에 실패할 경우 재시도 정책을 통해 5번까지 메시지 재처리하도록 하고, &lt;b&gt;모든 재시도에 실패할 경우 user.signed-up.dlt라는 DLT로 메시지를 이동 시킨다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개의 파티션에서 메시지를 병렬처리 하도록 하기 위해 concurrency값은 3으로 설정 하였다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.example.email_service;

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.RetryableTopic;
import org.springframework.retry.annotation.Backoff;
import org.springframework.stereotype.Service;

@Service
public class UserSignedUpEventConsumer {

    private EmailLogRepository emailLogRepository;

    public UserSignedUpEventConsumer(EmailLogRepository emailLogRepository) {
        this.emailLogRepository = emailLogRepository;
    }

    @KafkaListener(
            topics = &quot;user.signed-up&quot;, //이 토픽에서 메시지를 수신
            groupId = &quot;email-service&quot;,
            concurrency=&quot;3&quot; //파티션 개수만큼 설정
    )
    @RetryableTopic( //재시도 정책 정의
            attempts = &quot;5&quot;,
            backoff = @Backoff(delay=1000, multiplier = 2),
            dltTopicSuffix = &quot;.dlt&quot;
    )
    public void consume(String message) throws InterruptedException {
        UserSignedUpEvent userSignedUpEvent = UserSignedUpEvent.fromJson(message); //수신한 메시지를 객체로 변환

        //실제 이메일 발송 로직은 생략
        String receiverEmail = userSignedUpEvent.getEmail();
        String subject = userSignedUpEvent.getName()+ &quot;님, 회원 가입을 축하드립니다!&quot;;;
        Thread.sleep(3000);
        System.out.println(&quot;이메일 발송 완료&quot;);

        EmailLog emailLog = new EmailLog(
                userSignedUpEvent.getUserId(),
                receiverEmail,
                subject
                );

        emailLogRepository.save(emailLog);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>BackEnd/Kafka</category>
      <category>Kafka</category>
      <category>MSA</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/255</guid>
      <comments>https://developerwoohyeon.tistory.com/255#entry255comment</comments>
      <pubDate>Tue, 19 Aug 2025 16:37:57 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스 - 삼각 달팽이[Java]</title>
      <link>https://developerwoohyeon.tistory.com/254</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/68645&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/68645&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1755528825027&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;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/68645&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/YTNjh/hyZzAUNVqp/yvRSq3WhNasYAmXBas8lV1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bj8GAQ/hyZzIkY5Pd/ilBKplTApv81iAjlN3QpF1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/68645&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/68645&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/YTNjh/hyZzAUNVqp/yvRSq3WhNasYAmXBas8lV1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bj8GAQ/hyZzIkY5Pd/ilBKplTApv81iAjlN3QpF1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;문제 해결 과정&lt;/b&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;/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;는 n번, 그 다음 삼각형에서는 n-3번&lt;/li&gt;
&lt;li&gt;첫번째 삼각형에서 &lt;b&gt;오른쪽으로 이동 횟수&lt;/b&gt;는 n-1번, 그 다음 삼각형에서는 n-3-1번&lt;/li&gt;
&lt;li&gt;첫번째 삼각형에서 &lt;b&gt;대각선 위로 이동하는 횟수&lt;/b&gt;는 n-2번, 그 다음 삼각형에서는 n-3-2번&lt;/li&gt;
&lt;li&gt;아래 -&amp;gt; 오른쪽 -&amp;gt; 대각선 위 순서로 이동 후 len-3&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1755528795686&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int[] solution(int n) {
        int[][] arr = new int[n][n];
        int startX=0;
        int startY=0;
        int value=1;
        int size=0;
        int len=n;
        while(len&amp;gt;0){
            for(int i=0; i&amp;lt;len; i++){ //아래
                arr[startX][startY]=value;
                value++;
                startX++;
                size++;
                if(i==len-1){startX--;}
            }
            for(int i=0; i&amp;lt;len-1; i++){ //오른쪽
                startY++;
                arr[startX][startY]=value;
                size++;
                value++;
            }
            for(int i=0; i&amp;lt;len-2; i++){ //대각선
                startX--;
                startY--;
                arr[startX][startY]=value;
                size++;
                value++;
            }
            len=len-3;
            startX++;
        }
        int[] answer = new int[size];
        int index=0;
        for(int i=0; i&amp;lt;n; i++){
            for(int j=0; j&amp;lt;n; j++){
                if(arr[i][j]!=0){
                    answer[index]=arr[i][j];
                    index++;
                }
            }
        }
        
        return answer;
    }
    
    
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>삼각 달팽이</category>
      <category>프로그래머스</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/254</guid>
      <comments>https://developerwoohyeon.tistory.com/254#entry254comment</comments>
      <pubDate>Mon, 18 Aug 2025 23:59:29 +0900</pubDate>
    </item>
  </channel>
</rss>