<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>늘</title>
    <link>https://giron.tistory.com/</link>
    <description>알고리즘 + 스프링 백앤드 공부 기록 일지 ~.~
</description>
    <language>ko</language>
    <pubDate>Wed, 15 Apr 2026 01:03:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>giron</managingEditor>
    <image>
      <title>늘</title>
      <url>https://tistory1.daumcdn.net/tistory/4171903/attach/6b1be817ee184fd69184e6b4d5d2d9d4</url>
      <link>https://giron.tistory.com</link>
    </image>
    <item>
      <title>데이터독 모니터링과 함께 성능 개선하기</title>
      <link>https://giron.tistory.com/174</link>
      <description>&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;/h4&gt;
&lt;p data-end=&quot;160&quot; data-start=&quot;126&quot; data-ke-size=&quot;size18&quot;&gt;TTI (Time To Interactive)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;웹 페이지가 사용자 입력(클릭, 스크롤 등)에 즉시 반응할 수 있을 만큼 완전히 로드된 시점까지 걸린 시간&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;즉, 페이지가 시각적으로 보이기 시작한 후, 완전히 &quot;쓸 수 있는 상태&quot;가 될 때까지 걸리는 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;160&quot; data-start=&quot;126&quot; data-ke-size=&quot;size18&quot;&gt;Percentile (퍼센타일)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;160&quot; data-start=&quot;126&quot;&gt;전체 응답 시간(또는 성능 데이터)을 정렬한 후, 특정 백분위 위치에 있는 값&lt;/li&gt;
&lt;li data-end=&quot;638&quot; data-start=&quot;572&quot;&gt;P95 &amp;rarr; 95%의 요청은 이 시간보다 빠르고, 나머지 5%는 더 느리다는 의미&lt;/li&gt;
&lt;li data-end=&quot;668&quot; data-start=&quot;639&quot;&gt;P99 &amp;rarr; 가장 느린 상위 1% 요청의 기준점&lt;/li&gt;
&lt;li data-end=&quot;668&quot; data-start=&quot;639&quot;&gt;p99=500ms이면 99%요청은 500ms이하로 처리된다는 의미&lt;/li&gt;
&lt;/ul&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최적:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #0000ff;&quot;&gt;1초 미만&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;양호:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #0000ff;&quot;&gt;1~3초&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보통:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 3~5초&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개선 필요: 5초 ~&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API하나가 3초를 넘어가는것도 사실 개선이 필요해보이긴 하다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;적용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일주일간 p95지표를 확인하면 도메인 특성상 트래픽이 많아지는 점심시간에 p95속도가 느려집니다. 이러한 문제를 개선하고자 했습니다. 요청량이 많은 api중 p95 Latencey가 높은 API를 선정했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-17 오전 1.54.45.png&quot; data-origin-width=&quot;3174&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJJgci/btsN0BZQzvg/cnnQi0iEiECEw4uvveHuYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJJgci/btsN0BZQzvg/cnnQi0iEiECEw4uvveHuYk/img.png&quot; data-alt=&quot;p95 지표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJJgci/btsN0BZQzvg/cnnQi0iEiECEw4uvveHuYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJJgci%2FbtsN0BZQzvg%2FcnnQi0iEiECEw4uvveHuYk%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;3174&quot; height=&quot;464&quot; data-filename=&quot;스크린샷 2025-05-17 오전 1.54.45.png&quot; data-origin-width=&quot;3174&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;p95 지표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;1626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn6duU/btsN2dC4Muw/AwZCQRtkyae0KDm6zN12Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn6duU/btsN2dC4Muw/AwZCQRtkyae0KDm6zN12Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn6duU/btsN2dC4Muw/AwZCQRtkyae0KDm6zN12Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn6duU%2FbtsN2dC4Muw%2FAwZCQRtkyae0KDm6zN12Dk%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;3086&quot; height=&quot;1626&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;1626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 API의 로직을 확인했을때, 병목이 생길만한 쿼리를 찾았고 데이터독의 DBM을 통해서 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3298&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BZThT/btsN0NMql4N/iRR0CKn3fraHK2u9dPDpc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BZThT/btsN0NMql4N/iRR0CKn3fraHK2u9dPDpc0/img.png&quot; data-alt=&quot;병목 쿼리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BZThT/btsN0NMql4N/iRR0CKn3fraHK2u9dPDpc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBZThT%2FbtsN0NMql4N%2FiRR0CKn3fraHK2u9dPDpc0%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;3298&quot; height=&quot;138&quot; data-origin-width=&quot;3298&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;병목 쿼리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;하지만 5%정도의 규모가 큰 매장의 경우 위의 지표처럼 안좋은 사용자 경험을 주고 있습니다.&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;이를 개선하기 위해서 캐시를 적용하여 매장 재방문 간격을 캐시에 pre-loading하도록 개선하였습니다. 이를통해 성능 개선을 경험할 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-02 오후 1.37.30.png&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MCesK/btsO0UQPU6O/SMniXjVd9sGViOqqZRGYR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MCesK/btsO0UQPU6O/SMniXjVd9sGViOqqZRGYR1/img.png&quot; data-alt=&quot;배포후 Latency 지표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MCesK/btsO0UQPU6O/SMniXjVd9sGViOqqZRGYR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMCesK%2FbtsO0UQPU6O%2FSMniXjVd9sGViOqqZRGYR1%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;2596&quot; height=&quot;798&quot; data-filename=&quot;스크린샷 2025-07-02 오후 1.37.30.png&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;배포후 Latency 지표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백앤드 개발일지</category>
      <category>데이터독</category>
      <category>퍼센타일</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/174</guid>
      <comments>https://giron.tistory.com/174#entry174comment</comments>
      <pubDate>Wed, 9 Jul 2025 23:11:39 +0900</pubDate>
    </item>
    <item>
      <title>NestedLoop Join을 Hash Join으로 변경하여 쿼리 속도 개선</title>
      <link>https://giron.tistory.com/173</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 정산 쿼리가 있었는데 쿼리 길이만 152줄(with절을 통해 cte 테이블을 만들고 이를 중첩 join 사용)이 되었다. 실제로 매우 느렸던 문제를 이론으로 접하고 있던 hashJoin을 적용함으로써 성능 개선했던 경험을 작성하고자 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.5만개의 데이터를 O(n**2)으로 처리-&amp;gt; 12억번 조회해야한다. 실행계획을 확인하면 NestedLoop Join이 여러번 사용되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 데이터는 NestedLoop Join(O(N*M)보다 Hash Join(O(N+M) 혹은 Sort Merge JoinO(N log N + M log M)을 사용하면 개선 여지가 있다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Hash Join&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;454&quot; data-start=&quot;361&quot;&gt;&lt;b&gt;Build Phase&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;454&quot; data-start=&quot;384&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;454&quot; data-start=&quot;384&quot;&gt;조인 대상 테이블 중 &lt;u&gt;작은 테이블&lt;/u&gt;(보통 메모리에 올릴 수 있는 쪽)을 기준으로 해시 테이블을 생성 (조인 키 기준)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Probe Phase&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;478&quot; data-end=&quot;528&quot;&gt;큰 테이블의 각 행을 순회하면서, 조인 키를 해시 테이블에 탐색하여 매칭되는 값 찾음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해시의 특성상 동등(=) 조인 조건에서만 사용 가능하다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;단점으로는 CPU사용률이 높아진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sort Merge Join&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랜덤 액세스가 많을때 적용하기 좋다.&lt;/li&gt;
&lt;li&gt;정렬된 테이블을 조인할때 성능이 더 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;사용 방법&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용법은 간단하다. Postgresql에서 아래처럼 nestloop 옵션을 꺼줍니다. (해당 세션에만 적용됩니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1741693138052&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show enable_nestloop;

SET enable_nestloop = OFF;

SET enable_nestloop = ON;&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 큰 데이터들에 대해서는 2시간 가까이 걸리는 쿼리가 3분만에 나오기도 한다. 하지만 규모가 작은 데이터에 Hash Join을 적용하면 오히려 느려지는 경우를 확인할 수 있었다.&lt;/p&gt;</description>
      <category>백앤드 개발일지/데이터베이스</category>
      <category>hash join</category>
      <category>nested loop join</category>
      <category>sort merge join</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/173</guid>
      <comments>https://giron.tistory.com/173#entry173comment</comments>
      <pubDate>Sat, 17 May 2025 13:41:37 +0900</pubDate>
    </item>
    <item>
      <title>[큰 수]자바와 파이썬 그리고 BigInteger</title>
      <link>https://giron.tistory.com/172</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현업에서 파이썬 서비스를 jvm환경으로 이관하면서 파이썬에서 적용되던 코드를 이관하고 있었습니다. 이때 파이썬의 int는 무한한 길이의 숫자를 사용할 수 있어서 자바의 기본적인 long, double로는 처리가 안되는 문제를 발견했습니다. 이러한 이유에 대해서 분석한 내용을 기록합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-16 오후 10.27.16.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bez2uV/btsLQPq1Q7L/1RC0IX3CPyp7OsxdGZetM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bez2uV/btsLQPq1Q7L/1RC0IX3CPyp7OsxdGZetM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bez2uV/btsLQPq1Q7L/1RC0IX3CPyp7OsxdGZetM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbez2uV%2FbtsLQPq1Q7L%2F1RC0IX3CPyp7OsxdGZetM0%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;796&quot; height=&quot;116&quot; data-filename=&quot;스크린샷 2025-01-16 오후 10.27.16.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파이썬이 무한한 길이의 정수가 가능한 이유&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Arbitrary Precision(임의의 정확도)&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;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: left;&quot;&gt;파이썬의 int는 C 언어의&amp;nbsp;&lt;/span&gt;&lt;b&gt;long 타입&lt;/b&gt;을 기반으로 구현되어있습니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 자리수를 배열에 담아서 처리하며&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;배열에 각 자릿수를 담을때는 2^30 진법으로 변환함으로써 수행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; ob_digit이라는 배열에 해당 값을 담습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;예시&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;123456789101112131415와 같이 자바에서 처리하지 못하는 변수를 파이썬에서는 아래와 같이 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;7&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt;7&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt;&amp;lowast;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2^&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&amp;lowast;&lt;/span&gt;&lt;span&gt;0)&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;8&lt;/span&gt;&lt;span&gt;7&lt;/span&gt;&lt;span&gt;7&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&amp;lowast;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2^&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&amp;lowast;&lt;/span&gt;&lt;span&gt;1&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;1&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;7&lt;/span&gt;&lt;span&gt;&amp;lowast;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2^&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&amp;lowast;&lt;/span&gt;&lt;span&gt;2&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;ob_diti: [437976919 / &lt;/span&gt;&lt;span&gt;87719511 / &lt;/span&gt;&lt;span&gt;107&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;정리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 배열에 무수히 많은 숫자가 들어가고 각 자리수가 2^30을 담을 수 있으므로 무한한 숫자를 표현이 가능하다고 한것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;c 정수 -&amp;gt; python 정수&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737034399720&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHIFT = 30  # 각 자릿수를 나타낼 비트 갯수
MASK = (2 ** SHIFT)
bignum = 18446744073709551615

def split_number(bignum):
    t = abs(bignum)

    num_list = []
    while t != 0:
        # 나머지를 얻는다
        small_int = t % MASK  # 좀 더 효율적인 비트 연산: (t &amp;amp; (MASK-1))
        num_list.append(small_int)

        # 나눗셈의 정수부를 얻는다 (나눗셈 내림)
        t = t // MASK  # 좀 더 효율적인 비트 연산: t &amp;gt;&amp;gt;= SHIFT

    return num_list

def restore_number(num_list):
    bignum = 0
    for i, n in enumerate(num_list):
        bignum += n * (2 ** (SHIFT * i))
    return bignum

num_list = split_number(bignum)
assert bignum == restore_number(num_list)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 자바에서도 파이썬처럼 배열로 표현하면 가능하지 않을까 싶은 생각이 듭니다. 자바는 어떻게 처리가 될까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바의 BigInteger&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RBZVF/btsLPxykcfx/SaMxOmgIKDMNOtQ0ulc29K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RBZVF/btsLPxykcfx/SaMxOmgIKDMNOtQ0ulc29K/img.png&quot; data-alt=&quot;aribitrary-precision&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RBZVF/btsLPxykcfx/SaMxOmgIKDMNOtQ0ulc29K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRBZVF%2FbtsLPxykcfx%2FSaMxOmgIKDMNOtQ0ulc29K%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;614&quot; height=&quot;600&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;aribitrary-precision&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 문서 첫페이지에 임의의 정밀도가 보입니다.&amp;nbsp;자바의 BigInteger또한 파이썬과 비슷하게 동작하며&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;숫자를&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;비트 기반 배열&lt;/b&gt;에 저장합니다. 자세한건 코드를 보면서 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigInteger 생성자는 여러 개의 부생성자들을 가지고 있습니다. 대표적으로 2개의 생성자를 살펴보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;첫 번째는 BigInteger.valueOf()를 호출할때 내부에서 사용되는 생성자입니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;long값을 받기에 최대 배열2개만을 이용하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;private BigInteger(long val) {
    if (val &amp;lt; 0) {  // 부호 결정 
        val = -val;
        signum = -1;
    } else {
        signum = 1;
    }

    int highWord = (int)(val &amp;gt;&amp;gt;&amp;gt; 32); ----(1)
    if (highWord == 0) { -----------------(2)
        mag = new int[1];
        mag[0] = (int)val;
    } else {------------------------------(3)
        mag = new int[2];
        mag[0] = highWord;
        mag[1] = (int)val;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 상위 32비트를 추출하여 highWord에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 상위 32비트가 0인 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;long 값이 &lt;b&gt;32비트 이하&lt;/b&gt;라는 뜻이므로, 크기 1의 배열을 생성합니다.&lt;/li&gt;
&lt;li&gt;배열의 첫 번째 요소에 val의 하위 32비트를 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 상위 32비트가 0이 아닌 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;long 값이 &lt;b&gt;32비트를 초과&lt;/b&gt;하므로, 크기 2의 배열을 생성합니다.&lt;/li&gt;
&lt;li&gt;배열의 첫 번째 요소에 highWord(상위 32비트)를 저장합니다.&lt;/li&gt;
&lt;li&gt;배열의 두 번째 요소에 val의 하위 32비트를 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;두 번째는 public 생성자를 통해 무수히 큰 수를 처리하는 방식입니다.  &lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;radix는 디폴트값이 10입니다.(10진법을 의미)&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;/**
 * Translates the String representation of a BigInteger in the
 * specified radix into a BigInteger.  The String representation
 * consists of an optional minus or plus sign followed by a
 * sequence of one or more digits in the specified radix.  The
 * character-to-digit mapping is provided by {@link
 * Character#digit(char, int) Character.digit}.  The String may
 * not contain any extraneous characters (whitespace, for
 * example).
 *
 * @param val String representation of BigInteger.
 * @param radix radix to be used in interpreting {@code val}.
 * @throws NumberFormatException {@code val} is not a valid representation
 *         of a BigInteger in the specified radix, or {@code radix} is
 *         outside the range from {@link Character#MIN_RADIX} to
 *         {@link Character#MAX_RADIX}, inclusive.
 */
public BigInteger(String val, int radix) {
    int cursor = 0, numDigits;
    final int len = val.length();

    if (radix &amp;lt; Character.MIN_RADIX || radix &amp;gt; Character.MAX_RADIX)
        throw new NumberFormatException(&quot;Radix out of range&quot;);
    if (len == 0)
        throw new NumberFormatException(&quot;Zero length BigInteger&quot;);

    // Check for at most one leading sign
    int sign = 1;
    int index1 = val.lastIndexOf('-');
    int index2 = val.lastIndexOf('+');
    if (index1 &amp;gt;= 0) {
        if (index1 != 0 || index2 &amp;gt;= 0) {
            throw new NumberFormatException(&quot;Illegal embedded sign character&quot;);
        }
        sign = -1;
        cursor = 1;
    } else if (index2 &amp;gt;= 0) {
        if (index2 != 0) {
            throw new NumberFormatException(&quot;Illegal embedded sign character&quot;);
        }
        cursor = 1;
    }
    if (cursor == len)
        throw new NumberFormatException(&quot;Zero length BigInteger&quot;);

    // Skip leading zeros and compute number of digits in magnitude
    while (cursor &amp;lt; len &amp;amp;&amp;amp;
           Character.digit(val.charAt(cursor), radix) == 0) {
        cursor++;
    }

    if (cursor == len) {
        signum = 0;
        mag = ZERO.mag;
        return;
    }

    numDigits = len - cursor;
    signum = sign;

    // Pre-allocate array of expected size. May be too large but can
    // never be too small. Typically exact.
    long numBits = ((numDigits * bitsPerDigit[radix]) &amp;gt;&amp;gt;&amp;gt; 10) + 1;
    if (numBits + 31 &amp;gt;= (1L &amp;lt;&amp;lt; 32)) {
        reportOverflow();
    }
    int numWords = (int) (numBits + 31) &amp;gt;&amp;gt;&amp;gt; 5;
    int[] magnitude = new int[numWords];

    // Process first (potentially short) digit group
    int firstGroupLen = numDigits % digitsPerInt[radix];
    if (firstGroupLen == 0)
        firstGroupLen = digitsPerInt[radix];
    String group = val.substring(cursor, cursor += firstGroupLen);
    magnitude[numWords - 1] = Integer.parseInt(group, radix);
    if (magnitude[numWords - 1] &amp;lt; 0)
        throw new NumberFormatException(&quot;Illegal digit&quot;);

    // Process remaining digit groups
    int superRadix = intRadix[radix];
    int groupVal = 0;
    while (cursor &amp;lt; len) {
        group = val.substring(cursor, cursor += digitsPerInt[radix]);
        groupVal = Integer.parseInt(group, radix);
        if (groupVal &amp;lt; 0)
            throw new NumberFormatException(&quot;Illegal digit&quot;);
        destructiveMulAdd(magnitude, superRadix, groupVal);
    }
    // Required for cases where the array was overallocated.
    mag = trustedStripLeadingZeroInts(magnitude);
    if (mag.length &amp;gt;= MAX_MAG_LENGTH) {
        checkRange();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡해보이기때문에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;예시를 보면서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;핵심만 짚고 넘어가겠습니다. (설명이 생략된 부분은 검증과 0 생략처리 부분입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;123456789101112131415&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-17 오전 12.01.37.png&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd4oMI/btsLRliIgMk/zQBswmVsoQbBOODkrOmRbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd4oMI/btsLRliIgMk/zQBswmVsoQbBOODkrOmRbk/img.png&quot; data-alt=&quot;디버깅 정보&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd4oMI/btsLRliIgMk/zQBswmVsoQbBOODkrOmRbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd4oMI%2FbtsLRliIgMk%2FzQBswmVsoQbBOODkrOmRbk%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;826&quot; height=&quot;338&quot; data-filename=&quot;스크린샷 2025-01-17 오전 12.01.37.png&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디버깅 정보&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. numDigits는 숫자 자릿수를 계산합니다. 123456789101112131415의 경우 자릿수는 21입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. numBits는 숫자를 표현하기 위한 비트 수를 계산하는 데 사용됩니다. 각 자릿수는 radix(여기서는 10진법)에 맞는 비트 수를 가집니다. 이 과정에서, 각 숫자가 bitsPerDigit[10]에 따라 몇 비트로 표현되는지 계산됩니다. ((21 * 3402) &amp;gt;&amp;gt;&amp;gt; 10) + 1 = 70&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 결국에는 int numWords = (int) (numBits + 31) &amp;gt;&amp;gt;&amp;gt; 5; 파이썬과 비슷하게 2^5= 32비트 정수 배열에 필요한 크기를 구합니다.(=3)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개의 배열에 32비트로 변환한 값들을 담습니다. 앞의 그룹부터 처리하고 남은 크기가 있으면 반복하면서 그룹에 값을 담습니다.&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;mag&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;6: 숫자의 &lt;b&gt;상위 32비트&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;-1320247403: 숫자의 &lt;b&gt;중간 32비트&lt;/b&gt; (음수는 2의 보수 표현으로 저장됨).&lt;/li&gt;
&lt;li&gt;-635764905: 숫자의 &lt;b&gt;하위 32비트&lt;/b&gt; (음수는 2의 보수 표현).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;bitCountPlusOne = 0&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자의 &lt;b&gt;1로 설정된 비트 개수&lt;/b&gt;를 저장하는 캐시입니다. 이 값은 아직 계산되지 않았기 때문에 기본값 0입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;bitLengthPlusOne = 68&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비트 길이&lt;/b&gt;가 68로 저장되었습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 123456789101112131415의 &lt;b&gt;2진수 표현&lt;/b&gt;에서 가장 높은 비트까지의 길이를 나타냅니다.&lt;/li&gt;
&lt;li&gt;실제 비트 길이는 68 - 1 = 67 비트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;lowestSetBitPlusTwo = 0&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자의 &lt;b&gt;가장 낮은 1의 위치&lt;/b&gt;를 나타내는 필드입니다.&lt;/li&gt;
&lt;li&gt;아직 계산되지 않았고, 기본값 0으로 표시됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;firstNonzeroIntNumPlusTwo = 0&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;mag 배열에서 &lt;b&gt;가장 처음으로 0이 아닌 값&lt;/b&gt;이 나타나는 위치입니다.&lt;/li&gt;
&lt;li&gt;아직 계산되지 않았기 때문에 기본값 0입니다.&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;결국에는 파이썬과 비슷하게 숫자를 &lt;b&gt;32비트로 표현하는 배열&lt;/b&gt;에 담아서 무한한(배열이 메모리를 초과하지 않는 이상) 숫자를 가질 수 있게 된 것입니다. (숫자를 쪼개서 저장)&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://peps.python.org/pep-0237/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://peps.python.org/pep-0237/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/c-api/structures.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/3/c-api/structures.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737033649252&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;Common Object Structures&quot; data-og-description=&quot;There are a large number of structures which are used in the definition of object types for Python. This section describes these structures and how they are used. Base object types and macros: All ...&quot; data-og-host=&quot;docs.python.org&quot; data-og-source-url=&quot;https://docs.python.org/3/c-api/structures.html&quot; data-og-url=&quot;https://docs.python.org/3/c-api/structures.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AR17g/hyX0sq8rUt/4KfgpoXsMl14ampu3HxDpk/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/c-api/structures.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.python.org/3/c-api/structures.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AR17g/hyX0sq8rUt/4KfgpoXsMl14ampu3HxDpk/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200');&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;Common Object Structures&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;There are a large number of structures which are used in the definition of object types for Python. This section describes these structures and how they are used. Base object types and macros: All ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.python.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@toezilla/1D1Q-001.-Python%EC%9D%98-int-%EC%9E%90%EB%A3%8C%ED%98%95%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B2%94%EC%9C%84%EA%B0%80-%EB%AC%B4%EC%A0%9C%ED%95%9C%EC%9D%BC%EA%B9%8C#02-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%B3%80%EC%88%98%EC%9D%98-%ED%8A%B9%EC%A7%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@toezilla/1D1Q-001.-Python%EC%9D%98-int-%EC%9E%90%EB%A3%8C%ED%98%95%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B2%94%EC%9C%84%EA%B0%80-%EB%AC%B4%EC%A0%9C%ED%95%9C%EC%9D%BC%EA%B9%8C#02-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%B3%80%EC%88%98%EC%9D%98-%ED%8A%B9%EC%A7%95&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/자바</category>
      <category>BigInteger</category>
      <category>큰수</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/172</guid>
      <comments>https://giron.tistory.com/172#entry172comment</comments>
      <pubDate>Fri, 17 Jan 2025 21:43:46 +0900</pubDate>
    </item>
    <item>
      <title>멀티스레드와 ThreadPoolExecutor</title>
      <link>https://giron.tistory.com/171</link>
      <description>&lt;h1 style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-toc-skip=&quot;&quot;&gt;ThreadPoolExecutor&lt;/h1&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 style=&quot;background-color: #ffffff; color: #2c4557; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ThreadPoolExecutor 메서드&lt;/h3&gt;
&lt;pre id=&quot;code_1734251074724&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, // corePoolSize: 기본 스레드 수. 항상 최소 2개의 스레드는 유지됩니다.
    4, // maximumPoolSize: 최대 스레드 수. 필요할 경우 스레드는 최대 4개까지 증가할 수 있습니다.
    30, // keepAliveTime: corePoolSize를 초과하는 스레드가 대기할 수 있는 최대 시간(30초).
    TimeUnit.SECONDS, // keepAliveTime의 시간 단위. 여기서는 초(seconds) 단위로 설정됩니다.
    new LinkedBlockingQueue&amp;lt;&amp;gt;(10), // 작업 큐: 최대 10개의 작업을 담을 수 있는 LinkedBlockingQueue 사용.
    new ThreadPoolExecutor.DiscardPolicy() // 거부 정책: 큐가 가득 차면 새 작업을 그냥 버립니다.
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;일반적으로 작업당 호출 오버헤드가 감소하여 많은 수의 비동기 작업을 실행할 때 성능이 향상되고, 작업 모음을 실행할 때 소비되는 스레드를 포함한 리소스를 제한하고 관리하는 수단을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머는 일반적인 사용 시나리오에 대한 설정을 미리 구성한&lt;span&gt;&amp;nbsp;아래의&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: start;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html&quot;&gt;Executors&lt;/a&gt;팩토리 메서드를 사용하는 것이 좋다. (물론 내부적으로 ThreadPoolExecutor()를 사용해 구현되어있기에 커스텀이 필요하다면 ThreadPoolExecutor()를 직접 구현하면 된다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html#newCachedThreadPool()&quot;&gt;Executors.newCachedThreadPool()&lt;/a&gt;(무제한 스레드 풀, 자동 스레드 회수 포함)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리 시간이 짧은 비동기 io 테스크에 적합&lt;/li&gt;
&lt;li&gt;요청이 올때마다 스레드가 생성됨&lt;/li&gt;
&lt;li&gt;유휴 스레드가 있으면 해당 스레드를 사용하고 없으면 추가함.&lt;/li&gt;
&lt;li&gt;사용하지 않는 스레드는 60초 후 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html#newFixedThreadPool(int)&quot;&gt;Executors.newFixedThreadPool(int)&lt;/a&gt; (고정 크기 스레드 풀)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cpu bound나 트래픽이 예측가능할때 사용, cpu 코어 개수에 맞춰 개수 조정&lt;/li&gt;
&lt;li&gt;일정 양만큼 스레드가 생성되고 작업이 쌓이면 큐에 넣어놓고 스레드가 유휴일때 큐에서 꺼내서 작업 진행&lt;/li&gt;
&lt;li&gt;스레드 수가 고정이라 메모리 안정&lt;/li&gt;
&lt;li&gt;사용되지 않는 스레드는 제거되지 않음, shutdown()을 호출하면 스레드 제거 됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패로 종료되면 스레드가 자동 생성되어서 나머지 작업 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html#newSingleThreadExecutor()&quot;&gt;Executors.newSingleThreadExecutor()&lt;/a&gt;(단일 백그라운드 스레드)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 개의 스레드만 생성후 실행&lt;/li&gt;
&lt;li&gt;실패로 종료되면 스레드가 자동 생성되어 나머지 작업 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;Core and maximum pool sizes&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;ThreadPoolExecutor는 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;corePoolSize와 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;maximumPoolSize가 설정된 경계에 따라 자동으로 pool size를 조정합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;만약 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;corePoolSize보다 적게 스레드가 운영중이라면, 요청을 처리하기 위한 새로운 스레드가 만들어집니다. 비록 다른 워커 스레드들이 idel(유휴)상태&lt;/span&gt;&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: #474747; text-align: start;&quot;&gt;만약 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;maximumPoolSize보다 적게 스레드가 운영중이라면, 오직 &lt;u&gt;큐가 가득 찼을때만 스레드가 만들어진다&lt;/u&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;corePoolSize = &lt;/span&gt;maximumPoolSize, 같은 수로 세팅하면 고정된 poolSize를 생성할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;maximumPoolSize를 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;Integer.MAX_VALUE처럼 세팅하면 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;concurrent tasks 수 만큼 pool이 조정된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: start;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html#setCorePoolSize(int)&quot;&gt;setCorePoolSize(int)&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: start;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html#setMaximumPoolSize(int)&quot;&gt;setMaximumPoolSize(int)&lt;/a&gt; 를 사용하면 &lt;b&gt;&lt;u&gt;동적&lt;/u&gt;&lt;/b&gt;으로 바꿀수도 있다.&amp;nbsp;(더 작은 값으로 설정하면, 초과된 기존&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt; 스레드들은 다음에 유휴 상태가 될 때 종료된다.)&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;On-demand construction&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: start;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html#prestartCoreThread()&quot;&gt;prestartCoreThread()&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;or&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;prestartAllCoreThreads()를 사용해서 &lt;b&gt;&lt;u&gt;동적&lt;/u&gt;&lt;/b&gt;으로 overried할수 있다. 이를 통해 요청이 들어오기전에 미리 생성할 수 있다. 만약 이미 생성되어있다면 false를 반환한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;Keep-alive times&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;만약 pool이 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;corePoolSize threads 보다 많다면, &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;keepAliveTime이후의 유휴 상태의 초과한 스레드들은 삭제될것이다.&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: start;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html#setKeepAliveTime(long,java.util.concurrent.TimeUnit)&quot;&gt;setKeepAliveTime(long, TimeUnit)&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;.를 통해 &lt;u&gt;&lt;b&gt;동적&lt;/b&gt;&lt;/u&gt;으로 조정할 수 있다. 일반적으로 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;corePoolSize보다 많은 스레드를 대상으로 적용되지만 &lt;a style=&quot;background-color: #ffffff; color: #bb7a2a; text-align: start;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html#allowCoreThreadTimeOut(boolean)&quot;&gt;allowCoreThreadTimeOut(boolean)&lt;/a&gt;를 사용해서 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;core threads또한 대상에 적용되게 할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;Queuing&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #474747; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;corePoolSize보다 적은 수의 스레드가 실행 중이면 Executor는 큐에 넣는 것보다 항상 새로운 스레드를 추가하는 것을 선호합니다.&lt;/li&gt;
&lt;li&gt;corePoolSize 이상의 스레드가 실행 중이면 Executor는 항상 새로운 스레드를 추가하는 것보다 요청을 큐에 넣는 것을 선호합니다.&lt;/li&gt;
&lt;li&gt;요청을 큐에 넣을 수 없는 경우 새 스레드가 생성되지만, 이것이 maximumPoolSize를 초과하는 경우 작업은 거부됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 큐잉 전략&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Direct handoffs
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;좋은 기본 선택은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: left;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/SynchronousQueue.html&quot;&gt;SynchronousQueue&lt;/a&gt; 사용이다. 큐에&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;작업을 보류하지 않고 스레드에 작업을 핸드오프하는 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;일반적으로 새로 제출된 작업을 거부하지 않도록 무제한 maximumPoolSizes가 필요합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt; &lt;span style=&quot;background-color: #ffffff; text-align: justify;&quot;&gt;버퍼링 할 공간이 없기 때문에 Queue에 삽입하려는 동작과 Queue에서 가져가려 한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;Unbounded queues&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span&gt;예시로는 기본 생성자로 생성한 &lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #bb7a2a; text-align: left;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/LinkedBlockingQueue.html&quot;&gt;LinkedBlockingQueue&lt;/a&gt;가 있다. 큐 사이즈에 제한이 없다(Integer.MAX_VALUE).&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;기본 생성자로 큐를 생성하면 corePoolSize 스레드보다 더 많은 스레드가 생성되지 않습니다 (&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;maximumPoolSize가 무의미해진다.)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;각 작업이 다른 작업과 완전히 독립적이어서 작업이 서로의 실행에 영향을 미칠 수 없는 경우(예: 웹 페이지 서버에서) 이 방식이 적합할 수 있습니다. 이러한 스타일의 대기열은 일시적인 요청 버스트를 매끄럽게 처리하는 데 유용할 수 있지만 명령이 처리할 수 있는 것보다 평균적으로 더 빨리 도착하면 무제한 작업 대기열이 증가할 가능성이 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;Bounded queues&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;예시로&amp;nbsp; &lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: left;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ArrayBlockingQueue.html&quot;&gt;ArrayBlockingQueue&lt;/a&gt;가 있다.&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt; 매개변수 있는 생성자를 사용한 LinkedBlockingQueue를 사용해도 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;maximumPoolSizes의 자원이 고갈되는 것을 방지하도록 돕지만 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;조정 및 제어가 더 어려울 수 있다.( &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;maximum pool sizes와 Queue Size 설정이 trade-off가 있기 때문이다.)&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;/li&gt;
&lt;li&gt;큰 queue size와 작은 pool size는 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;CPU 사용, OS 리소스 및 컨텍스트 전환 오버헤드가 최소화되지만 인위적으로 처리량이 낮아질 수 있습니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;만약 IO bound처럼 자주 blocking된다면, 작은 queue size와 큰 pool size는 cpu를 더 바쁘게할수 있지만 오버헤드에 주의해야한다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;Rejected tasks&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 작업이 &lt;b&gt;execute(Runnable)&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;&lt;b&gt;Executor가 종료(shut down)된 경우.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Executor가 최대 스레드 수와 작업 큐의 크기 모두 유한한 제한(finite bounds)을 갖고 있고, 이미 포화 상태(saturated)인 경우&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4가지 방식으로 처리된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #474747; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #4a6782;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.AbortPolicy.html&quot;&gt;ThreadPoolExecutor.AbortPolicy&lt;/a&gt;: 기본 정책으로 핸들러는&lt;span&gt;&amp;nbsp;&lt;/span&gt;거부시 런타임에 &lt;a style=&quot;color: #4a6782;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/RejectedExecutionException.html&quot;&gt;RejectedExecutionException&lt;/a&gt;를 throw한다.&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;background-color: #ffffff; color: #4a6782; text-align: left;&quot; href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.CallerRunsPolicy.html&quot;&gt;ThreadPoolExecutor.CallerRunsPolicy&lt;/a&gt;:&amp;nbsp;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;작업을 제출한 &lt;b&gt;호출 스레드가 직접 거부된 작업을 실행&lt;/b&gt;하도록 합니다. (&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;응답이 느려질 수 있으나 받을 수 있음)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이 정책은 피드백 제어 메커니즘의 역할을 하며, 작업 제출 속도를 느리게 만드는 효과가 있습니다.&lt;br /&gt;즉, 제출한 스레드가 실행에 시간을 소비하게 되므로, 새로운 작업 제출이 줄어듭니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.DiscardPolicy.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ThreadPoolExecutor.DiscardPolicy&lt;/a&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행할 수 없는 작업을 &lt;b&gt;그냥 버립니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;작업 결과가 반드시 필요하지 않은 경우에만 사용해야 하며, 드물게 사용하는 정책입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.DiscardOldestPolicy.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ThreadPoolExecutor.DiscardOldestPolicy&lt;/a&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Executor가 종료되지 않은 경우, 작업 큐의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;가장 오래된 작업을 삭제&lt;/b&gt;하고, 새 작업 실행을 재시도합니다.&lt;/li&gt;
&lt;li&gt;단, 이 정책은 작업이 계속 거부될 수 있어, 대부분의 경우 적합하지 않습니다.&lt;br /&gt;작업을 삭제할 경우, 삭제된 작업이 대기 중인 컴포넌트에서 예외를 발생시키거나 작업 실패를 기록해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;500개의 동시 요청 테스트&amp;nbsp;&lt;br /&gt;&lt;br /&gt;1. CallerRunsPolicy: 8s&lt;br /&gt;2. DiscardPolicy: 5s&lt;br /&gt;3. DiscardOldestPolicy: 5s&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용자 정의 RejectedExecutionHandler&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;직접 RejectedExecutionHandler 클래스를 정의&lt;/b&gt;해 특정 요구 사항에 맞는 거부 정책을 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;사용자 정의 정책을 설계할 때는 특히 주의가 필요합니다. 특정 &lt;b&gt;작업 큐 크기&lt;/b&gt;나 &lt;b&gt;용량 정책&lt;/b&gt;에 맞게 설계해야 안정적인 작동이 보장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1734251007051&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(&quot;Task rejected: &quot; + r.toString());
        // 필요시 로깅 또는 알림 처리
    }
}

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 30, TimeUnit.SECONDS,
    new LinkedBlockingQueue&amp;lt;&amp;gt;(10),
    new CustomRejectedExecutionHandler()
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;shutDown&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shutDown():&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt; 기존에 들어있는 작업들은 모두 끝마치고 종료된다.&lt;/span&gt;&lt;br /&gt;shutDownNow():&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&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: #000000; text-align: left;&quot;&gt;cf. ExecutorService vs ForkJoinPool&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;이해하기 쉬운 자료가 있어서 남겨둔다. : &lt;a href=&quot;https://medium.com/@javatechie/understanding-the-basics-of-executorservice-vs-forkjoinpool-0fb22f117480&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@javatechie/understanding-the-basics-of-executorservice-vs-forkjoinpool-0fb22f117480&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;ExecutorService는 독립적으로 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734278480478&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExecutorService kitchen = Executors.newFixedThreadPool(3);

Future&amp;lt;String&amp;gt; soup = kitchen.submit(() -&amp;gt; &quot;Soup is ready!&quot;);
Future&amp;lt;String&amp;gt; steak = kitchen.submit(() -&amp;gt; &quot;Steak is ready!&quot;);
Future&amp;lt;String&amp;gt; salad = kitchen.submit(() -&amp;gt; &quot;Salad is ready!&quot;);

System.out.println(soup.get());  // Waits for soup
System.out.println(steak.get()); // Waits for steak
System.out.println(salad.get()); // Waits for salad

kitchen.shutdown();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ForkJoinPool은 각각의 결과를 합치는 과정이 들어간다.&lt;/p&gt;
&lt;pre id=&quot;code_1734278512881&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ForkJoinPool kitchen = new ForkJoinPool();

RecursiveTask&amp;lt;String&amp;gt; lasagnaTask = new RecursiveTask&amp;lt;&amp;gt;() {
    @Override
    protected String compute() {
        // Break down tasks like cooking noodles, making sauce, etc.
        ForkJoinTask&amp;lt;String&amp;gt; noodlesTask = new RecursiveTask&amp;lt;&amp;gt;() {
            protected String compute() {
                return &quot;Noodles cooked!&quot;;
            }
        }.fork();

        ForkJoinTask&amp;lt;String&amp;gt; sauceTask = new RecursiveTask&amp;lt;&amp;gt;() {
            protected String compute() {
                return &quot;Sauce prepared!&quot;;
            }
        }.fork();

        return noodlesTask.join() + &quot; &quot; + sauceTask.join() + &quot; Lasagna is ready!&quot;;
    }
};

String result = kitchen.invoke(lasagnaTask);
System.out.println(result);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ForkJoinPool은 하나의 스레드가 하나의 큐를 관리한다. 이때 한쪽의 스레드만 busy한 문제를 해결하기 위해 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;work-stealing 알고리즘을 사용한다.&amp;nbsp;&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지</category>
      <category>synchronousqueue</category>
      <category>thread pool</category>
      <category>ThreadPoolExecutor</category>
      <category>병렬처리</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/171</guid>
      <comments>https://giron.tistory.com/171#entry171comment</comments>
      <pubDate>Mon, 30 Dec 2024 21:57:36 +0900</pubDate>
    </item>
    <item>
      <title>[DockerLayerCache] CircleCi에서 dlc를 활용한 시간 단축</title>
      <link>https://giron.tistory.com/170</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&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;634&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmkH99/btsLlHBhAHE/ecQ7bEraonZCnc0j5JskgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmkH99/btsLlHBhAHE/ecQ7bEraonZCnc0j5JskgK/img.png&quot; data-alt=&quot;job&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmkH99/btsLlHBhAHE/ecQ7bEraonZCnc0j5JskgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmkH99%2FbtsLlHBhAHE%2FecQ7bEraonZCnc0j5JskgK%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;634&quot; height=&quot;214&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;job&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3108&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CJJFU/btsLnypcwk4/jEqi4DzdvJLw9b8CC95Xok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CJJFU/btsLnypcwk4/jEqi4DzdvJLw9b8CC95Xok/img.png&quot; data-alt=&quot;step&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CJJFU/btsLnypcwk4/jEqi4DzdvJLw9b8CC95Xok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCJJFU%2FbtsLnypcwk4%2FjEqi4DzdvJLw9b8CC95Xok%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;3108&quot; height=&quot;130&quot; data-origin-width=&quot;3108&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;step&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 docker build할때 걸리는 시간이다. 매번 배포할때마다 배포 시간이 느려서 일의 효율성에 치명적이었다. 배포 시간을 잡아먹는 가장 큰 원인은 도커 빌드할때 걸리는 시간이었고 해당 문제를 해결해야겠다고 생각했다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GcfaZ/btsLlPzi8Vs/mEEBh4CpqYLTIlGNoForgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GcfaZ/btsLlPzi8Vs/mEEBh4CpqYLTIlGNoForgk/img.png&quot; data-alt=&quot;개선후 job&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GcfaZ/btsLlPzi8Vs/mEEBh4CpqYLTIlGNoForgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGcfaZ%2FbtsLlPzi8Vs%2FmEEBh4CpqYLTIlGNoForgk%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;620&quot; height=&quot;192&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선후 job&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3106&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZdPuZ/btsLlcuQX1H/1kUkpeblwBGkq7On9iIYM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZdPuZ/btsLlcuQX1H/1kUkpeblwBGkq7On9iIYM1/img.png&quot; data-alt=&quot;캐싱된 step&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZdPuZ/btsLlcuQX1H/1kUkpeblwBGkq7On9iIYM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZdPuZ%2FbtsLlcuQX1H%2F1kUkpeblwBGkq7On9iIYM1%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;3106&quot; height=&quot;124&quot; data-origin-width=&quot;3106&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;캐싱된 step&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Docker Layer&lt;/h4&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;FROM
&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;레이어 생성 O&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;COPY
&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;레이어 생성 O&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RUN
&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;레이어 생성 O&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Docker Build Cache&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nD7Qo/btsLj8ZXE1B/Sk1NwoirnWwbKt8SIAnmQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nD7Qo/btsLj8ZXE1B/Sk1NwoirnWwbKt8SIAnmQ0/img.png&quot; data-alt=&quot;https://docs.docker.com/build/cache/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nD7Qo/btsLj8ZXE1B/Sk1NwoirnWwbKt8SIAnmQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnD7Qo%2FbtsLj8ZXE1B%2FSk1NwoirnWwbKt8SIAnmQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;399&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.docker.com/build/cache/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;빌드 캐시가 작동하는 방식&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레이어가 변경되면 그 뒤에 오는 다른 모든 레이어도 영향을 받습니다. 명령이 있는 레이어가&lt;span&gt;&amp;nbsp;&lt;/span&gt;COPY무효화되면 그 뒤에 오는 모든 레이어도 다시 실행해야 합니다.&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;700&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q6Kpk/btsLjKZmLE8/mKl9Yy0kT4JmyEknl4btw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q6Kpk/btsLjKZmLE8/mKl9Yy0kT4JmyEknl4btw0/img.png&quot; data-alt=&quot;https://docs.docker.com/build/cache/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q6Kpk/btsLjKZmLE8/mKl9Yy0kT4JmyEknl4btw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq6Kpk%2FbtsLjKZmLE8%2FmKl9Yy0kT4JmyEknl4btw0%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;700&quot; height=&quot;218&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.docker.com/build/cache/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 서비스에서는 이러한 설정이 다 되어있음에도 빌드 속도가 느려서 의문이었습니다. 추가적으로 개선할 방법이 있는지 확인하는 중, github action을 사용한 docker build에서 이와 &lt;a href=&quot;https://fe-developers.kakaoent.com/2022/220414-docker-cache/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;비슷한 문제&lt;/a&gt;를 해결한 것을 확인하였고 비슷한 ci툴로써 해결 방법이 있겠다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 찾은 방법이 docker layer cache(이하 dlc)입니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;CircleCI와 Docker Layer Cache&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker 이미지는 Dockerfiles에서 빌드되고 Dockerfile의 각 명령은 이미지에 새 레이어를 만듭니다.&lt;/li&gt;
&lt;li&gt;docker build 또는 docker compose로 Docker 이미지를 빌드하면 DLC는 작업을 실행하는 머신 또는 원격 Docker 인스턴스에 연결된 볼륨에 개별 레이어를 저장합니다.&lt;/li&gt;
&lt;li&gt;다음에 이미지 빌드 작업을 실행할 때 CircleCI는 캐시에서 변경되지 않은 레이어를 검색합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #343434; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파이프라인 실행 사이에 Dockerfile이 동일하게 유지되는 경우 전체 이미지가 캐시에서 검색됩니다. 실행 사이에 Dockerfile에 변경 사항을 도입하는 경우 CircleCI는 변경 사항까지의 모든 레이어를 검색한 다음 새 Dockerfile을 기반으로 나머지 이미지를 빌드합니다. &lt;u&gt;실행 사이에 Dockerfile이 적게 변경될수록 이미지가 더 빨리 빌드됩니다.&lt;/u&gt;&lt;u&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #343434; text-align: start;&quot;&gt;즉, circle ci를 사용할때는 매번 새로운 환경에서 실행되므로 캐시 적용을 하더라도 매번 새로운 환경에서 실행되므로 효과를 보지 못했었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #343434; text-align: start;&quot;&gt;적용방법&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #343434; text-align: start;&quot;&gt;적용방법은 간단합니다. docker_layer_caching: true로 설정하면 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734444239596&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jobs:
  build:
    docker:
      - image: cimg/base:2023.04

    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true # &amp;lt;- dlc적용&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;dlc를 사용하면 job당 200 credits이 부과되니 비용을 고려하면서 사용해야합니다..&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Docker Layer Caching (DLC) 200 credits/job&lt;br /&gt;200 credits = 0.12달러&lt;br /&gt;https://circleci.com/pricing/price-list/&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Refenece&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://circleci.com/docs/docker-layer-caching/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://circleci.com/docs/docker-layer-caching/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://circleci.com/blog/config-best-practices-docker-layer-caching/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://circleci.com/blog/config-best-practices-docker-layer-caching/&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지</category>
      <category>circle ci</category>
      <category>DLC</category>
      <category>Docker</category>
      <category>docker layer cache</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/170</guid>
      <comments>https://giron.tistory.com/170#entry170comment</comments>
      <pubDate>Sun, 22 Dec 2024 21:41:58 +0900</pubDate>
    </item>
    <item>
      <title>[JVM] 인스턴스 변수는 초기화를 안 해도 되지만 지역 변수는 초기화해야만 하는 이유</title>
      <link>https://giron.tistory.com/166</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;832&quot; data-end=&quot;860&quot; data-ke-size=&quot;size26&quot;&gt;인스턴스 변수 (Instance Variable)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 객체가 독립적으로 가지는 변수, 객체의 상태 표현&lt;/li&gt;
&lt;li&gt;각 객체간 독립적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;832&quot; data-end=&quot;860&quot; data-ke-size=&quot;size26&quot;&gt;클래스 변수 (Class Variable)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static을 사용한 정적 변수, 클래스의 상태 저장&lt;/li&gt;
&lt;li&gt;모든 객체가 클래스 변수를 공유한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;860&quot; data-start=&quot;832&quot; data-ke-size=&quot;size26&quot;&gt;지역 변수 (Local Variable)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;937&quot; data-start=&quot;861&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;882&quot; data-start=&quot;861&quot;&gt;메서드나 블록 안에서 선언되는 변수&lt;/li&gt;
&lt;li data-end=&quot;905&quot; data-start=&quot;883&quot;&gt;&lt;b&gt;해당 블록 안에서만&lt;/b&gt; 사용 가능&lt;/li&gt;
&lt;li data-end=&quot;937&quot; data-start=&quot;906&quot;&gt;기본값이 없어서 &lt;b&gt;반드시 초기화&lt;/b&gt;하고 사용해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;인스턴스 변수와&amp;nbsp;클래스 변수의 초기화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면&amp;nbsp;초기화하지않고 선언만 해주었을때 아래의 코드의 결과는 무엇이 나올까요?&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722960411195&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class VariableTest {
    static int a; // 클래스 변수
    int a; // 인스턴스 변수
    public static void main(String[] args) {
    	int k=4; // 지역 변수
        System.out.println(a);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-07 오전 1.07.22.png&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GHKhC/btsIURNrOqA/8xIlZClJ1ucrmclbdCa2ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GHKhC/btsIURNrOqA/8xIlZClJ1ucrmclbdCa2ZK/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GHKhC/btsIURNrOqA/8xIlZClJ1ucrmclbdCa2ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGHKhC%2FbtsIURNrOqA%2F8xIlZClJ1ucrmclbdCa2ZK%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;474&quot; height=&quot;100&quot; data-filename=&quot;스크린샷 2024-08-07 오전 1.07.22.png&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 0이 나옵니다. 만약 String 객체로 바꾼다면 null이 나오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;b&gt;지역&amp;nbsp;변수&lt;/b&gt;로 바꾸면 어떻게 나올까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-07 오전 1.08.20.png&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IFXvc/btsIVj3Z2Lc/fpMiKXlCjRXu343ufeEC90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IFXvc/btsIVj3Z2Lc/fpMiKXlCjRXu343ufeEC90/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IFXvc/btsIVj3Z2Lc/fpMiKXlCjRXu343ufeEC90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIFXvc%2FbtsIVj3Z2Lc%2FfpMiKXlCjRXu343ufeEC90%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;1148&quot; height=&quot;304&quot; data-filename=&quot;스크린샷 2024-08-07 오전 1.08.20.png&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 초기화가 되어있지 않아서 컴파일에서 에러가 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;b&gt;&lt;u&gt;클래스/인스턴스 변수&lt;/u&gt;&lt;/b&gt;는 초기화를 하지 않아도 &lt;u&gt;자동으로 할당&lt;/u&gt;이 되는데, &lt;b&gt;지역 변수&lt;/b&gt;는 자동으로 &lt;u&gt;초기화를 시켜주지 않는&lt;/u&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;클래스 변수와 달리 &lt;b&gt;지역 변수는 &lt;u&gt;'준비'단계가 없습니다&lt;/u&gt;&lt;/b&gt;. 사실 클래스 변수는 초깃값이 두 번 할당됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 준비 단계에서는 시스템 초깃값이 자동으로 할당된다. ( o, 0.0, null, false ) -&amp;gt; 클래스/인스턴스 변수 초기화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 초기화 단계에서는 개발자가 정의한 초깃값이 할당된다. -&amp;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;마지막으로 정리하자면&amp;nbsp;클래스 로드는 5 단계로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;로딩 -&amp;gt; 검증 -&amp;gt; &lt;b&gt;준비&lt;/b&gt; -&amp;gt; 해석 -&amp;gt;초기화&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;준비&lt;/b&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;1. 지역 변수가 아닌 클래스 변수만 할당하며, 지역 변수는 객체가 인스턴스화될때 객체와 함께 자바 힙에 할당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 클래스 변수에 할당하는 초깃값은 해당 데이터 타입의 제로값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 데이터 저장 공간이 다르기에 &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;</description>
      <category>백앤드 개발일지/자바</category>
      <category>JVM</category>
      <category>바이트코드</category>
      <category>인스턴스 변수</category>
      <category>지역 변수</category>
      <category>컴파일</category>
      <category>클래스 변수</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/166</guid>
      <comments>https://giron.tistory.com/166#entry166comment</comments>
      <pubDate>Fri, 9 Aug 2024 01:10:58 +0900</pubDate>
    </item>
    <item>
      <title>[Redisson] Redis RedLock의 한계와 RLock과 RFencedLock</title>
      <link>https://giron.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis를 사용한 분산락을 구현할때 RedLock의 한계는 이미 잘 알려진 문제입니다. 간략히 짚고 넘어가자면 Java기반 서버에서 stw가 발생할때 또는 클락 드리프트 이슈가 발생할때 락 일관성이 깨질 수 있다는 것입니다.&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;span style=&quot;color: #000000; text-align: start;&quot;&gt;그렇다면 redis를 사용한 분산락은 어떻게 구현할 수 있을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;redlock알고리즘을 사용하는 이유가 클러스터 구조에서 일관성있는 락을 유지하기 위해 존재하는데, redlock을 사용하지 않는다면 어떤 식으로 redis의 java 클라이언트인 redisson이 락을 처리하는지 확인해보겠습니다.&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: #000000; text-align: start;&quot;&gt;redisson은 앞선 설명에 따라 Redlock알고리즘을 사용한 락킹을 depcrecated시켰습니다.&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;434&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4SXKU/btsIWij6gXF/BruZK9vHQvxgFBf2fzG3mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4SXKU/btsIWij6gXF/BruZK9vHQvxgFBf2fzG3mk/img.png&quot; data-alt=&quot;wiki&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4SXKU/btsIWij6gXF/BruZK9vHQvxgFBf2fzG3mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4SXKU%2FbtsIWij6gXF%2FBruZK9vHQvxgFBf2fzG3mk%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;434&quot; height=&quot;73&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;wiki&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWIeGg/btsITWAniv8/JkHwJd71SYmOY1Y6olLRPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWIeGg/btsITWAniv8/JkHwJd71SYmOY1Y6olLRPk/img.png&quot; data-alt=&quot;Redlock Deprecated&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWIeGg/btsITWAniv8/JkHwJd71SYmOY1Y6olLRPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWIeGg%2FbtsITWAniv8%2FJkHwJd71SYmOY1Y6olLRPk%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;770&quot; height=&quot;135&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redlock Deprecated&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;주석에도 설명이 있듯이 getLock메서드를 사용하라고 합니다. 그렇다면 getLock은 어떻게 되어있길래 RedLock대신 사용하라고 하는걸까요?&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;getLock&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsqA23/btsITRFR8Jl/qbInGqUU0Kt21IrrbEe5Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsqA23/btsITRFR8Jl/qbInGqUU0Kt21IrrbEe5Pk/img.png&quot; data-alt=&quot;getLock&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsqA23/btsITRFR8Jl/qbInGqUU0Kt21IrrbEe5Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsqA23%2FbtsITRFR8Jl%2FqbInGqUU0Kt21IrrbEe5Pk%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;748&quot; height=&quot;195&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;getLock&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;getLock은 RLock을 반환하며 non-fair locking입니다. 즉, 메소드가 반환하는 락은 비공정 락이므로, 쓰레드가 락을 획득하는 순서를 보장하지 않습니다. 그리고 failover 동안의 신뢰성을 높이기 위해 모든 연산은 &lt;b&gt;모든 Redis 슬레이브 노드로의 전파를 기다립니다&lt;/b&gt;.(WAIT)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; 여기서 RLock 객체가 수행하는 모든 Redis 명령 실행은 Redis 3.0에 도입된 WAIT 명령을 통해 동기화됩니다. 이를통해 기존에는 &lt;b&gt;master-slave&lt;/b&gt; 혹은 &lt;b&gt;각 노드간에 비동기로 통신&lt;/b&gt;하면 장애 복구동안 락이 해제될 확률이 높았던 확률을 많이 낮추게 됩니다. (락이 slave 노드들에게 전파될때까지 대기하기 때문에)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;하지만 확률을 낮추게 만들었을뿐 이러한 WAIT명령어도 완전한 일관성을 보장하지는 않으며 단지 최선의 선택이라고만 되어있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/wait/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;WAIT 문서&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java FencedLock&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FencedLock은 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html&quot;&gt;java.util.concurrent.locks.Lock&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot; data-preserver-spaces=&quot;true&quot;&gt;&amp;nbsp;인터페이스를 구현하는 분산 잠금입니다. FencedLock은 fencingToken &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;(네트워크나 일시 중지된 프로세스에서 긴 지연으로부터 시스템을 보호)&lt;/span&gt; 을 이용하여 동작합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdUsq/btsIWkIZjWm/Cp6uX7loMTxC66088O3G3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdUsq/btsIWkIZjWm/Cp6uX7loMTxC66088O3G3k/img.png&quot; data-alt=&quot;RFencedLock&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdUsq/btsIWkIZjWm/Cp6uX7loMTxC66088O3G3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqdUsq%2FbtsIWkIZjWm%2FCp6uX7loMTxC66088O3G3k%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;669&quot; height=&quot;168&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RFencedLock&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;이 토큰은 클라이언트가 &lt;b&gt;잠금을 획득할 때마다 증가하는 숫자&lt;/b&gt;이며 클라이언트는 이 토큰을 모든 외부 서비스에 전달하고 모든 요청에 ​​토큰을 포함해야 합니다. 서비스가 두 개의 다른 토큰으로 요청을 받은 경우 &lt;b&gt;가장 높은 토큰 번호가 있는 요청을 수락&lt;/b&gt;하고 다른 모든 요청은 거부합니다. 이렇게 하면 두 클라이언트가 동시에 잠금을 가지고 있어도 외부 서비스는 한 클라이언트와만 상호 작용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;아래는 예시입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723045009567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void testTokenIncrease() {
    RFencedLock lock = redisson.getFencedLock(&quot;lock&quot;);
    Long token1 = lock.lockAndGetToken();
    assertThat(token1).isEqualTo(1);
    lock.unlock();
    assertThat(token1).isEqualTo(1);

    Long token2 = lock.lockAndGetToken();
    assertThat(token2).isEqualTo(2);
    lock.unlock();

    lock.lock();
    assertThat(lock.getToken()).isEqualTo(3);
    lock.unlock();

    lock.lock(10, TimeUnit.SECONDS);
    assertThat(lock.getToken()).isEqualTo(4);
    lock.unlock();

    Long token4 = lock.tryLockAndGetToken();
    assertThat(token4).isEqualTo(5);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FencedLock은 RLock과 마찬가지로&amp;nbsp; &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;fairness(공정성)를 제공하지 않습니다. &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot; data-preserver-spaces=&quot;true&quot;&gt;FencedLock은 일관성과 분할 허용성을 제공하지만 가용성을 제공하지 않으므로 CAP 정리에 따르면 CP 개념이라고 합니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot; data-preserver-spaces=&quot;true&quot;&gt;성능 차이&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot; data-preserver-spaces=&quot;true&quot;&gt;thread: 10 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot; data-preserver-spaces=&quot;true&quot;&gt;호출횟수: 1000&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UllkF/btsIW4ZO3IP/IDyMsFY0RkFCUk2CnEYJx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UllkF/btsIW4ZO3IP/IDyMsFY0RkFCUk2CnEYJx1/img.png&quot; data-alt=&quot;성능 비교1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UllkF/btsIW4ZO3IP/IDyMsFY0RkFCUk2CnEYJx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUllkF%2FbtsIW4ZO3IP%2FIDyMsFY0RkFCUk2CnEYJx1%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;429&quot; height=&quot;93&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성능 비교1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #474747;&quot;&gt;thread:30 &lt;/span&gt;&lt;span style=&quot;color: #474747;&quot;&gt;호출 횟수: 1000&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;418&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWJD6H/btsIXeuxNMe/cgUGUBR73R9Jtogku486Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWJD6H/btsIXeuxNMe/cgUGUBR73R9Jtogku486Fk/img.png&quot; data-alt=&quot;성능 비교2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWJD6H/btsIXeuxNMe/cgUGUBR73R9Jtogku486Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWJD6H%2FbtsIXeuxNMe%2FcgUGUBR73R9Jtogku486Fk%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;418&quot; height=&quot;91&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;91&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성능 비교2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #474747;&quot;&gt;thread:10 &lt;/span&gt;&lt;span style=&quot;color: #474747;&quot;&gt;호출 횟수: 10000&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;414&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct5Ug7/btsIWfHLaGQ/AsL3eFf6kcAhPA38bk7Al0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct5Ug7/btsIWfHLaGQ/AsL3eFf6kcAhPA38bk7Al0/img.png&quot; data-alt=&quot;성능 비교3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct5Ug7/btsIWfHLaGQ/AsL3eFf6kcAhPA38bk7Al0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct5Ug7%2FbtsIWfHLaGQ%2FAsL3eFf6kcAhPA38bk7Al0%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;414&quot; height=&quot;94&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성능 비교3&lt;/figcaption&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers#810-fenced-lock&quot;&gt;https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers#810-fenced-lock&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;a href=&quot;https://www.javadoc.io/doc/org.redisson/redisson/latest/org/redisson/api/RFencedLock.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.javadoc.io/doc/org.redisson/redisson/latest/org/redisson/api/RFencedLock.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://redisson.org/glossary/java-fencedlock.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redisson.org/glossary/java-fencedlock.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>fencedlock</category>
      <category>fencing token</category>
      <category>lock</category>
      <category>REDIS</category>
      <category>redlock</category>
      <category>Rlock</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/165</guid>
      <comments>https://giron.tistory.com/165#entry165comment</comments>
      <pubDate>Fri, 9 Aug 2024 00:49:03 +0900</pubDate>
    </item>
    <item>
      <title>[CircuitBreaker] 게이트웨이에서 서킷 브레이커 설정시 주의할점</title>
      <link>https://giron.tistory.com/164</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이를 운영하던중 첫 적립 api를 호출했을때 아래의 예외가 터졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f8f8f8; color: #1d1c1d; text-align: left;&quot;&gt;java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 1000ms in 'contextWriteRestoringThreadLocals' (and no fallback has been configured)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이 구성은 spring cloud gateway로 구성되어있고 아래 zipkin에서 보면 response-time이 1s가 넘어가자 exception을 던지는 것으로 확인했다.&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-filename=&quot;blob&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlDr11/btsIeGlvNUJ/NvTtWBW8nfb5z0rdzvUWr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlDr11/btsIeGlvNUJ/NvTtWBW8nfb5z0rdzvUWr1/img.png&quot; data-alt=&quot;zipkin&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlDr11/btsIeGlvNUJ/NvTtWBW8nfb5z0rdzvUWr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlDr11%2FbtsIeGlvNUJ%2FNvTtWBW8nfb5z0rdzvUWr1%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;1886&quot; height=&quot;386&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;zipkin&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 gateway에서 response-timeout설정을 1s보다 길게 잡아두었는데 1000ms에서 예외가 터지는게 의아했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원인 찾기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 일반적인 response-timeout으로 예외가 발생하면 아래처럼 예외가 발생해야하는데 예외 내용도 달랐다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;org.springframework.web.server.ResponseStatusException:&amp;nbsp;504&amp;nbsp;GATEWAY_TIMEOUT&amp;nbsp;&quot;Response&amp;nbsp;took&amp;nbsp;longer&amp;nbsp;than&amp;nbsp;timeout:&amp;nbsp;PT2S&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 해당 예외가 터질때마다 circuitBreaker의 fail count가 올라갔다. 따라서 해당 exception이 circuit breaker에 걸어둔 exception 조건에 맞는 예외일 것이라고 가정했다 실제 circuit breaker를 제거한 상태에서는 아무리 지연되어도 response-timeout내에서는 정상 응답을 받았다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인으로는 circuit breaker를 추가하면 timelimiter의 default timeout기간이 1s이다. 따라서 해당 설정이 우선순위가 되어 http-client의 response timeout이 1s보다 길더라도 timelimiter에 걸려서 예외가 발생한 것이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;public class TimeLimiterConfig implements Serializable {
    private static final long serialVersionUID = 2203981592465761602L;
    private static final String TIMEOUT_DURATION_MUST_NOT_BE_NULL = &quot;TimeoutDuration must not be null&quot;;
    private Duration timeoutDuration = Duration.ofSeconds(1L);
    private boolean cancelRunningFuture = true;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추후 조치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;timelimiter timeout &amp;gt; metadata timeout &amp;gt; httpclient timeout 우선순위이므로 timelimiter의 timeout을 httpclient timeout보다 길게 잡으면&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;b&gt;metadata timeout &amp;gt; httpclient timeout &amp;gt; timelimiter timeout순서&lt;/b&gt;가 되도록 &lt;u&gt;timelimiter timeout을 httpclient timeout보다 길게&lt;/u&gt; 설정하면 좋을 것 같습니다. (circuit을 붙이고 때더라도 spring gateway에서 설정한 timeout을 일정하게 유지하기 위함)&lt;/span&gt;&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;spring:
  cloud:
    gateway:
      httpclient:
        connect-timeout: 1000
        response-timeout: 1s # 여기서는 1s가능.
    routes:
      - id: api-server
        uri: http://localhost:8081
        filters:
          - name: CircuitBreaker
            args:
              name: tmpCircuitBreaker
              fallbackUri: forward:/fallback/tmp
        metadata:
          connect-timeout: 1000
          response-timeout: 1000 # 1s로 적으면 1초 넘어도 exception안터짐&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1719506441170&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s
        cancel-running-future: false&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추가 사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timelimiter의 timeout-duration은 slow-call-duration보다 반드시 커야한다. 안그러면 서킷브레이커에 집계되기 전에 timeout예외가 터지기때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;slow-call-duration &amp;lt;= metadata timeout &amp;lt;= httpclient timeout &amp;lt; timelimiter timeout&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#route-metadata-configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#route-metadata-configuration&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719507934925&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Cloud Gateway&quot; data-og-description=&quot;This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them &quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#route-metadata-configuration&quot; data-og-url=&quot;https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#route-metadata-configuration&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b14rCV/hyWrMDSDkw/GsOphYZ64B5QKEOPhSpiiK/img.png?width=443&amp;amp;height=595&amp;amp;face=0_0_443_595&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#route-metadata-configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#route-metadata-configuration&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b14rCV/hyWrMDSDkw/GsOphYZ64B5QKEOPhSpiiK/img.png?width=443&amp;amp;height=595&amp;amp;face=0_0_443_595');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Gateway&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@im_zero/spring-cloud-gateway-circuit-breaker-time-limiter-5e3c26a62b4c&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@im_zero/spring-cloud-gateway-circuit-breaker-time-limiter-5e3c26a62b4c&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>CircuitBreaker</category>
      <category>spring cloud gateway</category>
      <category>timelimiter</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/164</guid>
      <comments>https://giron.tistory.com/164#entry164comment</comments>
      <pubDate>Sun, 30 Jun 2024 02:51:10 +0900</pubDate>
    </item>
    <item>
      <title>[postgreSQL] order by와 limit이 걸린 슬로우 쿼리 해결 방법</title>
      <link>https://giron.tistory.com/163</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;limit이 걸린 쿼리는 limit이 안걸린 쿼리보다 항상 빠르다? ----- (X)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 조회쿼리에서 limit이 걸리면 특정 개수만 조회하기에 항상 빨라질거라 생각했지만 limit을 해제하니 오히려 쿼리 속도가 더 빨라졌습니다. 어떻게 이런 일이 발생했는지 확인해보겠습니다.&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;/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;history테이블은 5억이상의 데이터가 적재되어있고 복합 인덱스가 걸려있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716615179670&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE history (
    id INT AUTO_INCREMENT PRIMARY KEY,
    active_id INT NULL,
    event_type VARCHAR(255) NOT NULL,
);

CREATE INDEX idx_active_id_event_type ON history(active_id, event_type);&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;그리고 아래 쿼리를 날려보았는데, 20초가 넘어도 조회가 되지 않았습니다. 심지어 select절에 id만 조회하도록 하여, 커버링 인덱스를 이용하도록 했음에도 불구하고 마찬가지였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716614516694&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id desc limit 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실행계획을 살펴보았습니다. 아래처럼 cost가 저렴함에도 불구하고 나오지 않아 의문이었습니다. 다만 걸렸던 것은 Index Scan BackWard using history_pkey on history 였습니다. (여기서 실행계획을 더 자세히 봤어야했습니다..)&lt;/p&gt;
&lt;pre id=&quot;code_1716615540818&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Limit  (cost=0.57..6637.16 rows=100 width=56)
  -&amp;gt;  Index Scan Backward using history_pkey on history  (cost=0.57..25093747.04 rows=378112 width=56)
        Filter: ((active_id IS NULL) AND (event_type = 'done'::ck_event_type))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 mySQL의 innodb와 마찬가지로 리프 노드가 더블 링크드 리스트로 이어져있습니다. 그렇기에 인덱스 스캔 순서가 큰 영향이 없을걸로 판단했습니다. 그래서 관련 문서를 더 찾아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes-ordering.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/docs/current/indexes-ordering.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716617146530&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;11.4.&amp;nbsp;Indexes and ORDER BY&quot; data-og-description=&quot;11.4.&amp;nbsp;Indexes and ORDER BY # In addition to simply finding the rows to be returned by a query, an index &amp;hellip;&quot; data-og-host=&quot;www.postgresql.org&quot; data-og-source-url=&quot;https://www.postgresql.org/docs/current/indexes-ordering.html&quot; data-og-url=&quot;https://www.postgresql.org/docs/16/indexes-ordering.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/txfY2/hyV9PBowi9/MOR8hV9RRWq7OWJKMqSJv1/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes-ordering.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.postgresql.org/docs/current/indexes-ordering.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/txfY2/hyV9PBowi9/MOR8hV9RRWq7OWJKMqSJv1/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557');&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;11.4.&amp;nbsp;Indexes and ORDER BY&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;11.4.&amp;nbsp;Indexes and ORDER BY # In addition to simply finding the rows to be returned by a query, an index &amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.postgresql.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NULLS LAST&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문서에서는 아래와 같이 적혀있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;An index stored in ascending order with nulls first can satisfy either&amp;nbsp;ORDER BY x ASC NULLS FIRST&amp;nbsp;or&amp;nbsp;ORDER BY x DESC NULLS LAST&amp;nbsp;depending on which direction it is scanned in.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Null이 먼저 오름차순으로 저장된 인덱스는 스캔되는 방향에 따라 ORDER BY x ASC NULLS FIRST 또는 ORDER BY x DESC NULLS LAST를 충족할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;NULL, NULL, &lt;/b&gt;1, 2, 3, 4, 5&lt;/b&gt;&amp;nbsp;이렇게 인덱스가 저장되어 있을때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ORDER BY x ASC NULLS FIRST&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 쿼리는 인덱스를 정방향으로 스캔합니다.결과 &amp;gt;&amp;gt; NULL, NULL, 1, 2, 3, 4, 5&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;ORDER BY x DESC NULLS LAST&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 쿼리는 인덱스를 역방향으로 스캔합니다.결과 &amp;gt;&amp;gt; 5, 4, 3, 2, 1, NULL, NULL&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;속는셈 치고 해당 문서에 따라 NULLS LAST를 추가하여 아래 쿼리를  날려보았습니다. 그리고 놀랍게도 빠르게 조회가 되는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716617435893&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id desc NULLS LAST limit 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 실행 계획을 분석해봤고 아래처럼 나왔습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716617561332&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Limit  (cost=998715.69..998715.94 rows=100 width=56)
  -&amp;gt;  Sort  (cost=998715.69..999660.97 rows=378112 width=56)
        Sort Key: id DESC NULLS LAST
        -&amp;gt;  Index Scan using ix_history_active_id_event_type on history  (cost=0.57..984264.52 rows=378112 width=56)
              Index Cond: ((active_id IS NULL) AND (event_type = 'done'::ck_event_type))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;p&lt;/span&gt;k인데 NULLS LAST를 추가한 것이 어떤 원인으로 개선되었는지 파악할 필요는 있을것 같습니다. 다만 확실히 Index Scan BackWard 가 사라진것을 확인했고 이것이 느린 원인이라고 생각했습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Index Scan BackWard&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;MySQL Innodb&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;index scan backward가 더 느린이유는 2가지가 있다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;페이지 잠금이&lt;span&gt;&amp;nbsp;&lt;/span&gt;Forward index scan에 적합한 구조&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;페이지 내에서 인덱스 레코드는 단방향으로만 연결된 구조 (Forwarded single linked link)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 이야기는 &lt;a href=&quot;https://tech.kakao.com/posts/351&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카카오 기술 블로그&lt;/a&gt;에 적혀있기에 참고해주시길 바랍니다. 요약하자면 &lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;페이지 잠금 과정에서 데드락을 방지하기 위해서 B-Tree의 왼쪽에서 오른쪽 순서(Forward)로만 잠금을 획득하도록 하고 있습니다. 따라서&lt;/span&gt;&amp;nbsp;backward scan은 &lt;span style=&quot;background-color: #ffffff; color: #374151; 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;&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;&lt;span&gt;다만 위는 innodb에서의 케이스이기에 postgresql에서 케이스를 봐보겠습니다. 아마 비슷할 것으로 추측합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;&lt;span&gt;PostgreSQL&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/postgres/postgres/blob/53785d2a2aaa7899eb82fb4eba9af6da83680c8d/src/backend/access/nbtree/nbtsearch.c#L1691&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;postgreSQL 코드&lt;/a&gt;를 보면 주석에서의 설명에서는 아래와 같습니다. 즉, 순방향 스캔에서와의 차이가 크지 않다고 하는데요.. &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;대부분의 경우 튜플의 인덱스 키를 검사하지 않고 다음 튜플로 건너뛰는 것이 좋습니다(이전에는 실제로 왜냐하면 우리는 거꾸로 스캔하고 있기 때문입니다). 그러나 이것이 페이지의 첫 번째 튜플인 경우 인덱스 키를 확인하여 불필요하게 왼쪽 페이지로 이동하는 것을 방지합니다. 이는 순방향 스캔에서 사용되는 하이 키 최적화와 유사합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;&lt;span&gt;만약 backward index scan으로 인해 느리다는 가설이 맞으려면 order by x asc를 했을땐 빨라야할것 같다고 생각해서 해당 조건으로 테스트해봤습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1716626988206&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id limit 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;하지만 Index scan Backward가 없었지만 느렸습니다..! 이때 자세히 보니 조건절에서 &lt;b&gt;Filter&lt;/b&gt;를 통해서 조건을 확인하는 것을 발견했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1716627052807&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Limit  (cost=0.57..6637.16 rows=100 width=56)
    -&amp;gt;  Index Scan using activity_pkey on activity  (cost=0.57..25097467.04 rows=378112 width=56)
        Filter: ((active_id IS NULL) AND (event_type = 'done'::ck_event_type))&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;LIMIT이 없으면 더 빨라진다?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가적으로 처음 보여주었던 쿼리는 Limit이 걸려있지만, 여기서 limit을 제거하면 오히려 더 빨라집니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음 보여주었던 쿼리는 아래와 같았습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1716626646066&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id desc limit 100&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 limit을 제거하고 실행 계획을 봐보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716626646066&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id desc&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1716626646067&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;Sort  (cost=1032219.18..1033164.46 rows=378112 width=56)
  Sort Key: id DESC
  -&amp;gt;  Index Scan using ix_history_active_id_event_type on history  (cost=0.57..984264.52 rows=378112 width=56)
        Index Cond: ((active_id IS NULL) AND (event_type = 'done'::ck_event_type))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네, Filter가 아니라 Index Cond를 통해 조회하는 것을 확인할 수 있습니다. 그러면 두개가 무슨 차이일까요?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Index Cond vs Filter&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Index Cond
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 스캔(Index Scan) 또는 비트맵 인덱스 스캔(Bitmap Index Scan) 시 사용되는 조건을 나타냅니다.&amp;nbsp;Index Cond는 인덱스를 사용하여 데이터를 검색할 때 적용되는 조건입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Filter
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 스캔이나 순차 스캔(Sequential Scan) 후에 적용되는 추가적인 필터링 조건을 나타냅니다. 즉, PostgreSQL이 이미 인덱스 조건(Index Cond)이나 다른 방식으로 데이터의 일부를 가져온 후, 그 결과 집합에 추가적으로 적용되는 조건입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 옵티마이저가 pk 인덱스를 타는게 더 빠르다고 판단해서 order by id로 전체 정렬을 한 후, filter로 조건에 따라 솎아내다보니 느려진것입니다. 그렇다면 어떻게 개선할수 있을까요? 이미 예전부터 많은 사람들이 겪은 문제였는지 해당 &lt;a href=&quot;https://www.postgresdba.com/bbs/board.php?bo_table=P01&amp;amp;wr_id=124&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;사이트&lt;/a&gt;에서 도움을 얻을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;pk에 변화를 가해서 인덱스를 타지 못하도록 한다. 이를 통해 조건으로 먼저 걸러낸 후 정렬하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 아래처럼 쿼리를 변경해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716627832734&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id+0 limit 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 이렇게 쿼리를 날리면 우선 Index Cond를 통해 조건에 맞는 결과만 조회하고 이를 정렬하는 실행 계획을 확인할 수 있으며 실제 쿼리 속도 빨리진 것을 확인했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slow query(order by + limit)&lt;/p&gt;
&lt;pre id=&quot;code_1716627920177&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id limit 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fast query&lt;/p&gt;
&lt;pre id=&quot;code_1716627980601&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from history where active_id is null and event_type = 'done' order by id
select * from history where active_id is null and event_type = 'done' limit 100
select * from history where active_id is null and event_type = 'done' order by id+0 limit 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;정렬하고 조건에 따라 구분하는 것&lt;/u&gt;보다 &lt;u&gt;&lt;b&gt;조건에 따라 구분하고 정렬하는 것이 더 빠른 쿼리&lt;/b&gt;&lt;/u&gt;였습니다. 하지만 옵티마이저는 pk인덱스를 우선 타는게 빠르다고 판단하여 쿼리를 실행했지만 더 느렸던 것입니다.&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;NULLS LAST를 넣어서 빨리진 것도 pk 인덱스에 정렬 방식에 변경이 가해져서 pk인덱스를 우선 타는것보다 인덱스 조건에따라 구분하는 것이 더 빠르다고 판단되어 우연히 맞아떨어진 것이였습니다..!&lt;/p&gt;</description>
      <category>백앤드 개발일지/데이터베이스</category>
      <category>backward index scan</category>
      <category>postgresql</category>
      <category>옵티마이저</category>
      <category>인덱스</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/163</guid>
      <comments>https://giron.tistory.com/163#entry163comment</comments>
      <pubDate>Sat, 25 May 2024 21:30:42 +0900</pubDate>
    </item>
    <item>
      <title>[CircuitBreaker] 서킷브레이커의 이벤트를 슬랙으로 알림받기</title>
      <link>https://giron.tistory.com/162</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서킷브레이커는 회로 차단기로도 불리며 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;주요 목적은 시스템의 일부분에 문제가 발생했을 때, 그 문제가 전체 시스템으로 확산되는 것을 방지하는 것입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTxQDL/btsG0Hkh57p/ZlKv3V52f7R3dG3EWIUFC1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTxQDL/btsG0Hkh57p/ZlKv3V52f7R3dG3EWIUFC1/img.jpg&quot; data-alt=&quot;state&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTxQDL/btsG0Hkh57p/ZlKv3V52f7R3dG3EWIUFC1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTxQDL%2FbtsG0Hkh57p%2FZlKv3V52f7R3dG3EWIUFC1%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;426&quot; height=&quot;164&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;state&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 게이트웨이에서 라우팅하는 대상의 서버가 응답이 없거나 특정 에러를 계속 발생시킨다면 게이트웨이의 자원도 고갈됩니다. 모든 서비스에 장애가 전파되는 것이지요. 이러한 문제를 막기 위해 회로차단기(&lt;u&gt;circuitBreaker&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;서킷 브레이커는 개발자가 설정하는 값에 따라 어느 상황에서 서킷을 열지를 결정할 수 있습니다. (&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker&quot;&gt;설정 관련 문서&lt;/a&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;h4 data-ke-size=&quot;size20&quot;&gt;1. 슬랙 준비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(슬랙 연동은 &lt;a href=&quot;https://velog.io/@king/slack-incoming-webhook&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;해당 블로그&lt;/a&gt;를 참고했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬랙 연동이 완료되면 슬랙의 채널에서 &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;757&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NJrqB/btsGZaulKFL/dad9OMTXJCFiwCcv8oNQZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NJrqB/btsGZaulKFL/dad9OMTXJCFiwCcv8oNQZ1/img.png&quot; data-alt=&quot;채널에 이 앱 추가 클릭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NJrqB/btsGZaulKFL/dad9OMTXJCFiwCcv8oNQZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNJrqB%2FbtsGZaulKFL%2Fdad9OMTXJCFiwCcv8oNQZ1%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;757&quot; height=&quot;199&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;199&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채널에 이 앱 추가 클릭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 코드 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서킷브레이커는 이벤트를 listen하는 곳을 EventConsumer라고 불립니다. 이벤트를 컨슘할 수 있는 클래스를 작성해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CircuitBreakerEventConsumer를 작성해주고 EventConsumer 인터페이스를 구현해줍니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2d2a2e; color: #fcfcfa;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CircuitBreakerEventConsumer implements EventConsumer&amp;lt;CircuitBreakerOnStateTransitionEvent&amp;gt; {
   private static final Logger logger = LoggerFactory.getLogger(&quot;CircuitBreakerEventConsumer&quot;);
   private final SlackNotificationService slackNotificationService;

   public CircuitBreakerEventConsumer(SlackNotificationService slackNotificationService) {
      this.slackNotificationService = slackNotificationService;
   }

   @Override
   public void consumeEvent(CircuitBreakerOnStateTransitionEvent transitionEvent) {
      if (transitionEvent.getStateTransition() == CLOSED_TO_OPEN) {
         logger.warn(&quot;[서킷] 열렸다!!!!!!!!&quot;);
         slackNotificationService.send(&quot;서킷 브레이커가 열렸습니다: &quot; + transitionEvent.getCircuitBreakerName());
      } 
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 슬랙 알림 서비스를 간단하게 구현해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2d2a2e; color: #fcfcfa;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class SlackNotificationService {

   private static final Logger logger = LoggerFactory.getLogger(&quot;SlackNotificationService&quot;);

   private final WebClient webClient = WebClient.create();

   private final ObjectMapper objectMapper;
   private final String slackWebhookUrl;

   public SlackNotificationService(@Value(&quot;${slack.webhook.url}&quot;) String slackWebhookUrl, ObjectMapper objectMapper) {
      this.slackWebhookUrl = slackWebhookUrl;
      this.objectMapper = objectMapper;
   }


   public void send(String message){
      Mono&amp;lt;String&amp;gt; resultMono  = sendSlackMessage(message);
      resultMono.doOnSuccess(result -&amp;gt; logger.info(&quot;Message sent successfully: &quot; + result))
         .doOnError(error -&amp;gt; logger.error(&quot;Error sending message: &quot; + error.getMessage()))
         .subscribe();
   }

   public Mono&amp;lt;String&amp;gt; sendSlackMessage(String message) {
      String jsonMessage = getMessage(message);

      return webClient.post()
         .uri(slackWebhookUrl)
         .bodyValue(jsonMessage)
         .retrieve()
         .bodyToMono(String.class);
   }

   private String getMessage(String message) {
      Map&amp;lt;String, String&amp;gt; messageMap = new HashMap&amp;lt;&amp;gt;();
      messageMap.put(&quot;text&quot;, message);

      try {
         return objectMapper.writeValueAsString(messageMap);
      } catch (JsonProcessingException e) {
         throw new RuntimeException(&quot;JSON 변환 중 오류 발생&quot;, e);
      }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 서킷브레이커의 이벤트를 등록시켜주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 서킷브레이커마다 다른 이벤트를 적용시킬수도 있고, 모든 서킷브레이커를 같은 이벤트를 consum할 수 있도록 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2d2a2e; color: #fcfcfa;&quot;&gt;&lt;code&gt;@Configuration
public class CircuitBreakerEventConfig {

   public CircuitBreakerEventConfig(CircuitBreakerRegistry circuitBreakerRegistry, CircuitBreakerEventConsumer circuitBreakerEventConsumer) {

      circuitBreakerRegistry.getAllCircuitBreakers()
         .forEach(it -&amp;gt; it.getEventPublisher().onStateTransition(circuitBreakerEventConsumer));

      // 각각의 서킷마다 이벤트 적용
      // CircuitBreaker circuitBreaker = circuitBreakerRegistry.find(&quot;exampleCircuitBreaker&quot;)
      //     .orElseThrow(IllegalArgumentException::new);
      //
      // circuitBreaker.getEventPublisher().onStateTransition(circuitBreakerEventConsumer);
   }

}&lt;/code&gt;&lt;/pre&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;343&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvbATY/btsGY3vgfEY/przChijFgFcZMgobeSh2Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvbATY/btsGY3vgfEY/przChijFgFcZMgobeSh2Sk/img.png&quot; data-alt=&quot;슬랙의 알림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvbATY/btsGY3vgfEY/przChijFgFcZMgobeSh2Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvbATY%2FbtsGY3vgfEY%2FprzChijFgFcZMgobeSh2Sk%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;343&quot; height=&quot;57&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;57&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;슬랙의 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지</category>
      <category>CircuitBreaker</category>
      <category>EVENT</category>
      <category>slack</category>
      <category>webflux</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/162</guid>
      <comments>https://giron.tistory.com/162#entry162comment</comments>
      <pubDate>Mon, 29 Apr 2024 00:54:32 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] JdbcPagingItemReader의 페이징은 offset을 사용하지 않는다</title>
      <link>https://giron.tistory.com/161</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;정확히는 &lt;u&gt;MySQL에서 PagingQueryProvider와 JdbcPagingItemReader을 함께 사용할때 페이징은 offset을 사용하지 않는다 &lt;/u&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문제 상황으로는 JdbcPagingItemReader를 통해 조회를 할때, &lt;u&gt;SELECT 절에 별칭을 주었는데 정상적으로 읽지 못하는 문제&lt;/u&gt;가 발생했습니다. 해당 이슈를 해결하면서 알게된 사실을 공유하려고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JdbcPagingItemReader&lt;/h3&gt;
&lt;pre id=&quot;code_1711981583592&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@StepScope
public JdbcPagingItemReader&amp;lt;Point&amp;gt; reader() {

    return new JdbcPagingItemReaderBuilder&amp;lt;Point&amp;gt;()
            .name(&quot;reader&quot;)
            .pageSize(chunkSize)
            .fetchSize(chunkSize)
            .dataSource(datasource)
            .rowMapper(pointRowMapper)
            .parameterValues(parameters)
            .queryProvider(pagingQueryProvider())
            .build();
}

@Bean
public PagingQueryProvider pagingQueryProvider() {

    SqlPagingQueryProviderFactoryBean queryProvider = new SqlPagingQueryProviderFactoryBean();
    queryProvider.setDataSource(datasource);
    queryProvider.setSelectClause(&quot;place_id, user_id as member_id, points&quot;);
    queryProvider.setFromClause(&quot;user_point&quot;);
    queryProvider.setWhereClause(&quot;place_id = :place_id&quot;);
    queryProvider.setSortKeys(sortKeyAsc(&quot;place_id&quot;, &quot;member_id&quot;));

    try {
        return queryProvider.getObject();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 PagingQueryProvider를 통해 페이징 쿼리를 만든다면 어떤 쿼리가 만들어져 나갈것으로 예상이 될까요?&lt;/p&gt;
&lt;pre id=&quot;code_1711981618128&quot; class=&quot;shell&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;SELECT place_id, user_id as member_id, point 
FROM user_point 
WHERE (user_point.place_id = ?) 
ORDER BY place_id ASC, member_id ASC 
OFFSET ?
LIMIT 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 offset기반으로 페이징이 나갈 것으로 예상을 할 것같은데요. 왜냐하면 JdbcPagingItemReader는 offset, limit기반으로 페이지네이션을 한다고 알고있기 때문입니다. 그래서 성능을 높이려면 cursorPagingItemReader나 ItemReader를 커스텀하게 구현하여 noOffset기반으로 사용하기도 합니다.&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;그런데 실제 위 reader가 실행되면 아래와 같은 쿼리가 나가게 됩니다.  &lt;/p&gt;
&lt;pre id=&quot;code_1711981367248&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT place_id, user_id as member_id, point 
FROM user_point 
WHERE (user_point.place_id = ?) 
AND ((place_id &amp;gt; ?) OR (place_id = ? AND member_id &amp;gt; ?)) 
ORDER BY place_id ASC, member_id ASC 
LIMIT 100&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위 상황을 알게 된 이유를 보자면, queryProvider에서 select절에 as 별칭을 주어 사용하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 SQL에서는 쿼리 순서상 SELECT이후 ORDER BY절을 타므로 user_id에 member_id로 별칭을 주고 member_id를 정렬의 기준으로 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 point테이블에서 member_id를 찾을 수 없다는 에러가 나옵니다..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별칭을 주었는데 ORDER BY에서 읽지를 못한다라...여기서부터 시작되었습니다.&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;DIG&lt;/h3&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;public abstract class AbstractSqlPagingQueryProvider implements PagingQueryProvider {
...
    public Map&amp;lt;String, Order&amp;gt; getSortKeysWithoutAliases() {
        Map&amp;lt;String, Order&amp;gt; sortKeysWithoutAliases = new LinkedHashMap();
        Iterator var2 = this.sortKeys.entrySet().iterator();

        while(var2.hasNext()) {
            Map.Entry&amp;lt;String, Order&amp;gt; sortKeyEntry = (Map.Entry)var2.next();
            String key = (String)sortKeyEntry.getKey();
            int separator = key.indexOf(46);
            if (separator &amp;gt; 0) {
                int columnIndex = separator + 1;
                if (columnIndex &amp;lt; key.length()) {
                    sortKeysWithoutAliases.put(key.substring(columnIndex), (Order)sortKeyEntry.getValue());
                }
            } else {
                sortKeysWithoutAliases.put((String)sortKeyEntry.getKey(), (Order)sortKeyEntry.getValue());
            }
        }

        return sortKeysWithoutAliases;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 따라 이를 구현한 PagingQueryProvider들이 여러개 있습니다. SqlPagingQueryProviderFactoryBean 클래스를 확인하면 여러 PagingQueryProvider를 확인할 수 있고, 저는 Mysql을 사용하기 때문에 &lt;u&gt;&lt;b&gt;MySqlPagingQueryProvider&lt;/b&gt;&lt;/u&gt; 를 확인해봐야 합니다. (실제 동작할때는 datasource에 맞춰서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SqlPagingQueryProviderFactoryBean이 알맞은 queryProvider 실행해줍니다.)&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MySqlPagingQueryProvider&lt;/h4&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;public class MySqlPagingQueryProvider extends AbstractSqlPagingQueryProvider {
    public MySqlPagingQueryProvider() {
    }

    public String generateFirstPageQuery(int pageSize) {
        return SqlPagingQueryUtils.generateLimitSqlQuery(this, false, this.buildLimitClause(pageSize));
    }

    public String generateRemainingPagesQuery(int pageSize) {
        return StringUtils.hasText(this.getGroupClause()) ? SqlPagingQueryUtils.generateLimitGroupedSqlQuery(this, this.buildLimitClause(pageSize)) : SqlPagingQueryUtils.generateLimitSqlQuery(this, true, this.buildLimitClause(pageSize));
    }

    private String buildLimitClause(int pageSize) {
        return &quot;LIMIT &quot; + pageSize;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;## generateFirstPageQuery&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 페이징 쿼리부터 확인해보겠습니다. ORDER BY절은 단순히 sortKeys를 사용해서 order by 구문을 만드는 곳입니다. 중요한 곳은 &lt;u&gt;&lt;b&gt;buildWhereClause&lt;/b&gt;&lt;/u&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;public static String generateLimitSqlQuery(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, String limitClause) {
    StringBuilder sql = new StringBuilder();
    sql.append(&quot;SELECT &quot;).append(provider.getSelectClause());
    sql.append(&quot; FROM &quot;).append(provider.getFromClause());
    buildWhereClause(provider, remainingPageQuery, sql);
    buildGroupByClause(provider, sql);
    sql.append(&quot; ORDER BY &quot;).append(buildSortClause(provider));
    sql.append(&quot; &quot;).append(limitClause);
    return sql.toString();
}&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;### buildWhereClause&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부의 buildSortConditions를 더 봐바야 할것 같습니다.&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;private static void buildWhereClause(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, StringBuilder sql) {
    if (remainingPageQuery) {
        sql.append(&quot; WHERE &quot;);
        if (provider.getWhereClause() != null) {
            sql.append(&quot;(&quot;);
            sql.append(provider.getWhereClause());
            sql.append(&quot;) AND &quot;);
        }

        buildSortConditions(provider, sql);
    } else {
        sql.append(provider.getWhereClause() == null ? &quot;&quot; : &quot; WHERE &quot; + provider.getWhereClause());
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;### buildSortConditions&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 buildWhereClause로 where절을 만드는 곳이라고 생각되었는데 SortConditions도 만드는 것을 볼 수 있습니다. 여기서 직접 페이징에 필요한 방식을 offset이 아닌 where절로 만드는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711984915683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void buildSortConditions(AbstractSqlPagingQueryProvider provider, StringBuilder sql) {
    List&amp;lt;Map.Entry&amp;lt;String, Order&amp;gt;&amp;gt; keys = new ArrayList(provider.getSortKeys().entrySet());
    List&amp;lt;String&amp;gt; clauses = new ArrayList();

    String prefix;
    for(int i = 0; i &amp;lt; keys.size(); ++i) {
        StringBuilder clause = new StringBuilder();
        prefix = &quot;&quot;;

        for(int j = 0; j &amp;lt; i; ++j) {
            clause.append(prefix);
            prefix = &quot; AND &quot;;
            Map.Entry&amp;lt;String, Order&amp;gt; entry = (Map.Entry)keys.get(j);
            clause.append((String)entry.getKey());
            clause.append(&quot; = &quot;);
            clause.append(provider.getSortKeyPlaceHolder((String)entry.getKey()));
        }

        if (clause.length() &amp;gt; 0) {
            clause.append(&quot; AND &quot;);
        }

        clause.append((String)((Map.Entry)keys.get(i)).getKey());
        if (((Map.Entry)keys.get(i)).getValue() != null &amp;amp;&amp;amp; ((Map.Entry)keys.get(i)).getValue() == Order.DESCENDING) {
            clause.append(&quot; &amp;lt; &quot;);
        } else {
            clause.append(&quot; &amp;gt; &quot;);
        }

        clause.append(provider.getSortKeyPlaceHolder((String)((Map.Entry)keys.get(i)).getKey()));
        clauses.add(clause.toString());
    }

    sql.append(&quot;(&quot;);
    String prefix = &quot;&quot;;
    Iterator var10 = clauses.iterator();

    while(var10.hasNext()) {
        prefix = (String)var10.next();
        sql.append(prefix);
        prefix = &quot; OR &quot;;
        sql.append(&quot;(&quot;);
        sql.append(prefix);
        sql.append(&quot;)&quot;);
    }

    sql.append(&quot;)&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이처럼 &lt;b&gt;&lt;/b&gt;&lt;u&gt;&lt;b&gt;Keyset Pagination&lt;/b&gt;&lt;/u&gt; 또는 &lt;b&gt;&lt;u&gt;noOffset&lt;/u&gt;&lt;/b&gt;과 같은 방식으로 구현되어있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;아무래도 이러한 이유로 spring batch docs에서도 고유한 sortKey를 사용하라는 것 같습니다. sortKey를 기반으로 정렬을 할때는 unique가 중요하기 때문입니다.&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;1183&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lyoab/btsGoQaC6LJ/kNFgY7g7dKYrGZKKjX7Pqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lyoab/btsGoQaC6LJ/kNFgY7g7dKYrGZKKjX7Pqk/img.png&quot; data-alt=&quot;unique sortKey&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lyoab/btsGoQaC6LJ/kNFgY7g7dKYrGZKKjX7Pqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flyoab%2FbtsGoQaC6LJ%2FkNFgY7g7dKYrGZKKjX7Pqk%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;1183&quot; height=&quot;170&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;unique sortKey&lt;/figcaption&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;span style=&quot;color: #000000; text-align: start;&quot;&gt;결론&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1. JdbcPagingItemReader는 offset기반 페이지네이션이 아닌 noOffset기반 페이지네이션으로 처리하고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2. 따라서 queryProvider에 alias로 별칭을 주면, 해당 별칭의 네이밍으로 where절에 조건에 들어가므로 컬럼을 찾을 수 없다는 예외가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;issue는 남겨두었다..:&lt;a href=&quot;https://github.com/spring-projects/spring-batch/issues/4573&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-batch/issues/4573&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백앤드 개발일지</category>
      <category>JdbcPagingItemReader</category>
      <category>queryProvider</category>
      <category>Spring Batch</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/161</guid>
      <comments>https://giron.tistory.com/161#entry161comment</comments>
      <pubDate>Sun, 7 Apr 2024 18:42:01 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch 5 와 ???</title>
      <link>https://giron.tistory.com/160</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;spring batch 5는 이전 버전과 많이 변경되었다. 기본적인 스프링배치부터 스프링 배치 5.0은 어떻게 달라졌는지 직접 실무에 적용해보면서 느꼈던 경험을 적어본다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bui83I/btsCx9mlFHJ/xWKVwPKtrl7kwnhKWxkbGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bui83I/btsCx9mlFHJ/xWKVwPKtrl7kwnhKWxkbGk/img.png&quot; data-alt=&quot;spring batch Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bui83I/btsCx9mlFHJ/xWKVwPKtrl7kwnhKWxkbGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbui83I%2FbtsCx9mlFHJ%2FxWKVwPKtrl7kwnhKWxkbGk%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;454&quot; height=&quot;520&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;spring batch Architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; Architecture&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://docs.spring.io/spring-batch/reference/spring-batch-architecture.html&quot;&gt;공식문서&lt;/a&gt;에 나온 아키텍처이다. Application, Core, and Infrastructure 로 구성되어있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Application: 애플리케이션에는 Spring Batch를 사용하여 개발자가 작성한 모든 배치 작업과 사용자 정의 코드가 포함되어 있다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Batch Core: 배치 작업을 시작하고 제어하는 데 필요한 핵심 런타임 클래스가 포함되어 있습니다. JobLauncher, Job, and Step이 포함되어있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Batch Infrastructure: 애플리케이션과 코어는 모두 공통 인프라 위에 구축된다. 이 인프라에는 readers(ItemReader) 와 writers(ItemWriter) 그리고 services (such as the RetryTemplate) 와 핵심 프레임워크 자체가 포함되어 있다 .&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; Principle and Guidelines&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치 어플리케이션 내에서 가능한한 복잡한 로직은 피하고&amp;nbsp;&lt;b&gt;단순&lt;/b&gt;하게 설계합니다.&lt;/li&gt;
&lt;li&gt;데이터 처리하는 곳과 데이터의 저장소는 물리적으로 가능한한 가까운 곳에 위치하게 합니다.&lt;/li&gt;
&lt;li&gt;불필요한 테이블 또는 인덱스 스캔이 발생하지 않도록 한다.&lt;/li&gt;
&lt;li&gt;WHERE SQL 문의 절 에 키 값을 지정해야 한다.&lt;/li&gt;
&lt;li&gt;처리 시간이 많이 걸리는 작업은, 시작하기 전 충분한 메모리를 할당해서 메모리 재할당에 시간을 소모되지 않도록 한다.&lt;/li&gt;
&lt;li&gt;가능한 경우 내부 검증을 위한 체크섬을 구현합니다.&amp;nbsp;예를 들어 플랫 파일에는 파일의 총 레코드와 키 필드의 집계를 알려주는 트레일러 레코드가 있어야 합니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;메타 테이블&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 여러 블로그에 남아있어서 간단하게 기록한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BATCH_JOB_EXECUTION&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;END_TIME: 실행이 종료된 시점을 TIME_STAMP로 기록하며, 실행 도중 오류 발생시 기록이 안될 수 있다.&lt;/li&gt;
&lt;li&gt;EXIT_MESSAGE: 실패했을때의 메시지로 어디서 어떤 예외가 터졌는지 trace가 남는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;BATCH_JOB_EXECUTION_CONTEXT&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SHORT_CONTEXT: 공유할 데이터를 넣어놓는 Map이다. base64로 인코딩되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BATCH_STEP_EXECUTION&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;END_TIME: 실행이 종료된 시점을 TIME_STAMP로 기록하며, 실행 도중 오류 발생시 기록이 안될 수 있다.&lt;/li&gt;
&lt;li&gt;read count, write count 및 commit count를 기록한다. 이를 이용해 retry, 예외 처리에서 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOB_INSTANCE는 하나만 생성되고 JOB_EXECUTION은 JOB이 실행될때마다 생성된다.(1:N 관계)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 여러 테이블이 있다.(총 9개)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chunk 아키텍처&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;1817&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdkSD2/btsCyxHeXHc/j8D0Nka7uXbRZRxahoaBw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdkSD2/btsCyxHeXHc/j8D0Nka7uXbRZRxahoaBw0/img.png&quot; data-alt=&quot;chunk architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdkSD2/btsCyxHeXHc/j8D0Nka7uXbRZRxahoaBw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdkSD2%2FbtsCyxHeXHc%2Fj8D0Nka7uXbRZRxahoaBw0%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;1817&quot; height=&quot;461&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;1817&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;chunk architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemProcessor가 iterator를 돌면서 처리한다. 따라서 processor에서 I/O작업은 없애는 것을 추천한다.&lt;/li&gt;
&lt;li&gt;chunkSize와 paging사이즈가 같아야, chunk사이즈 검사를 한번에 통과할 수 있다. 다르면 반복되기 때문에 같아야 한다.&lt;/li&gt;
&lt;li&gt;IteamReader, ItemProcessor는 Chunk내 개별 아이템을 처리한다. ItemWriter는 Chunk크기만큼 일괄 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cursor vs Paging ItemReader&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cursor: 커넥션이 한번 열리면, 배치가 끝날때까지 열리므로 db connection time이 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Cursor 방식으로 데이터를 읽어서 처리하는 경우 읽을 데이터가 존재하지 않을 때까지 DB 커넥션을 유지한채로 계속 데이터를 FetchSize 만큼 가지고 와서 한 건씩 read 하게 됩니다.&lt;br /&gt;그렇기 때문에 스트리밍 방식으로 계속 데이터를 메모리에 가지고 와서 처리하기 때문에 한번의 쿼리 결과로 조회된 데이터를 모두 처리할 때까지 커넥션이 유지되고 메모리에 적재되고 건건히 처리되는 방식입니다.&lt;br /&gt;물론 GC 가 적절한 시점에 메모리 정리를 하겠지만 커넥션이 이루어지고 닫히기 까지 하나의 트랜잭션 안에서 모든 데이터가 처리되기 때문에 메모리에 계속 할당한다고 볼 수 있다고 한다.&lt;br /&gt;참고로 Cursor 방식은 내부적으로 스냅샷 방식으로 동작하기 때문에 메모리 사용량이 많아지기도 한다고 한다.&lt;br /&gt;대신 페이징 방식은 커넥션이 이루어지고 쿼리를 실행한 후 페이징 크기 만큼 데이터를 처리하고 커넥션이 닫힙니다. 그리고 다시 커넥션이 이루어지고 쿼리를 새로 실행하는 방식으로 이루지기 때문에 페이징 크기만큼 생성된 트랜잭션 안에서 메모리 처리가 이루어진다고 볼 수 있습니다.&lt;br /&gt;출처: &lt;a href=&quot;https://www.inflearn.com/questions/450829/cursor-%EB%B0%A9%EC%8B%9D-itemreader-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.inflearn.com/questions/450829/cursor-%EB%B0%A9%EC%8B%9D-itemreader-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4&lt;/a&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;paging: 한 페이지 읽을때마다 커넥션을 끄고 키므로 대용량에 더 효율적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaCursorItemReader는 올바른 MySQL Cursor 방식이 아닙니다. 데이터를 DB에서 모두 읽고 서비스 인스턴스에서 직접 Iterator로 cursor로 동작하는 것처럼 흉내 내는 방식입니다. 즉, 모든 데이터를 메모리에 들고 있기 때문에 &lt;span data-token-index=&quot;1&quot;&gt;OOM을 유발합니다.&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;사이즈가 작을때 paging을 사용하면, 매번 커넥션을 맺고 연결하는 작업이 더 비쌀테니 이럴때는 cursor를 사용하는게 좋을 것 같다.(keep-alive가 나온 이유에서부터 생각해봤다)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ItemStream&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;open
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ExecutionContext에서 제공된 strem을 open한다. 저장된 실행상태를 읽어온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;update
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변화된 상태를 ExecutionContext에 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;close
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stream을 닫는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemReader나 ItemWriter를 커스텀하게 활용하려면 해당 인터페이스를 구현해야. 재시도시에 실패한 시점부터 잡을 실행시킬 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JobLauncherApplicationRunner&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: left;&quot;&gt;CommandLineJobRunner &lt;/span&gt;vs jobLauncher&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot web이 따로 없을때는 CommandLineJobRunner는 java -jar를 실행했을때 이용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 web이 있으면 jobLauncher를 사용하여 job을 실행시킬 수 있다. 또한 비동기로 돌아간다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-08 오전 1.04.01.png&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blFe3n/btsC4a6e8Pj/sBk70K6SDpAzDxAAxsLMH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blFe3n/btsC4a6e8Pj/sBk70K6SDpAzDxAAxsLMH1/img.png&quot; data-alt=&quot;Asynchronous Job Launcher Sequence From Web Container&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blFe3n/btsC4a6e8Pj/sBk70K6SDpAzDxAAxsLMH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblFe3n%2FbtsC4a6e8Pj%2FsBk70K6SDpAzDxAAxsLMH1%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;587&quot; height=&quot;389&quot; data-filename=&quot;스크린샷 2024-01-08 오전 1.04.01.png&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Asynchronous Job Launcher Sequence From Web Container&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job이 실행중인지 확인하려면 jobExplorer에서 job이 실행중인지 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1704644017524&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        Set&amp;lt;JobExecution&amp;gt; runningJobExecutions = jobExplorer.findRunningJobExecutions(jobName);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Job Incrementer()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;web환경에서 jobLauncher를 통해서 실행 &lt;span data-token-index=&quot;1&quot;&gt;jobLauncher.run(job, jobParameters);&lt;/span&gt;하면, job실행 이전에 jobParam의 설정을 마친다. 따라서 추가로 custom한 increment api가 동작하지 않는다. 따라&lt;span style=&quot;color: #000000;&quot;&gt;서 addString(&quot;&lt;a style=&quot;color: #000000;&quot; href=&quot;http://run.id/&quot; data-token-index=&quot;4&quot;&gt;run.id&lt;/a&gt;&quot;, new Date()) 처럼 직접 넣어주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB설정&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rewriteBatchedStatments=true
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connector/J가 쿼리를 서버에 개별적으로 제출하지 않으려고 하기 때문. 따라서 쿼리 중 하나가 실패하면 후속 쿼리가 실행되지 않는다. Mysql서버가 한번에 모아서 쿼리를 날리도록 해준다.&lt;/li&gt;
&lt;li&gt;따라서 쿼리 중 하나가 실패하면 후속 쿼리가 실행되지 않습니다. 반면 rewriteBatchedStatements=false를 사용하면 실패에 관계없이 모든 쿼리가 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Spring Batch 5.x&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 블로그들은 스프링 배치 5.0 이전 버전을 기준으로 설명되어있다. 따라서 추가적인 부분만 적어본다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. BatchAutoConfiguration (DefaultBatchConfiguration &amp;amp; EnableBatchProcessing)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 @EnableBatchProcessing 어노테이션을 통해서 스프링 배치의 스프링 부트 자동설정을 활성화할 수 있었다. 하지만 이제는 스프링 부트의 자동설정을 사용하기 위해서는 &lt;u&gt;삭제&lt;/u&gt;해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EnableBatchProcessing 명시하는 방법 또는 DefaultBatchConfiguration 을 상속하여 활성화되는 빈은 이제 스프링 부트의 자동설정을 밀어내고(back-off), 애플리케이션의 설정을 커스텀하는 용도로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 @EnableBatchProcessing 이나 DefaultBatchConfigration 을 사용하면 spring.batch.jdbc.initialize-schema 등의 기본 설정이 동작하지 않는다. 또한 부트를 실행시킬 때 Job 이 자동으로 실행되지 않으므로 Runner 의 구현이 필요하다.&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;Job 을 수행하는 JobLauncherApplicationRunner 빈을 생성&lt;/li&gt;
&lt;li&gt;5.0부터는 @EnableBatchProcessing, DefaultBatchConfituration을 설정하면 @ConditionalOnMissingBean에 의해&lt;span&gt;&amp;nbsp;&lt;/span&gt;JobLauncherApplicationRunner가 실행되지 않는다는 점을 꼭 기억하자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* @ConditionalOnMissingBean 은 동명의 빈이 정의되어있으면 해당 동명의 빈을 사용하고 아니면 현재 빈을 사용한다. 따라서 DefaultBatchConfiguration을 상속받으면 jobLauncherApplicationRunner도 구현해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1703430246342&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@AutoConfiguration(after = { HibernateJpaAutoConfiguration.class, TransactionAutoConfiguration.class })
@ConditionalOnClass({ JobLauncher.class, DataSource.class, DatabasePopulator.class })
@ConditionalOnBean({ DataSource.class, PlatformTransactionManager.class })
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)
@EnableConfigurationProperties(BatchProperties.class)
@Import(DatabaseInitializationDependencyConfigurer.class)
public class BatchAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnProperty(prefix = &quot;spring.batch.job&quot;, name = &quot;enabled&quot;, havingValue = &quot;true&quot;, matchIfMissing = true)
	public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer,
			JobRepository jobRepository, BatchProperties properties) {
		JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository);
		String jobNames = properties.getJob().getName();
		if (StringUtils.hasText(jobNames)) {
			runner.setJobName(jobNames);
		}
		return runner;
	}
    ...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. mutlple job(다중 잡)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이제 단일 Job 을 감지하면 부트가 실행될 때 Job 을 실행시킵니다. 만약 여러 개의 Job 이 context 에 존재한다면, 부트를 실행할 때 spring.batch.job.name 을 통해 실행시킬 Job 을 명시해줘야 한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;그렇지 않으면 Caused by: java.lang.IllegalArgumentException: Job name must be specified in case of multiple jobs 이러한 에러를 마주한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;spring-configuration-metadata.json 을 보면 job.enabled는 default가 true인데, 모든 jobs들을 실행시킨다. 그러므로 job.name을 반드시 명시해주자.&lt;/li&gt;
&lt;li&gt;참고로 job.name={job1}, {job2} 이렇게 이름을 주면 job을 찾지 못한다. 하나의 job을 명시해주자&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-11 오후 4.42.42.png&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lN5HI/btsCyLrPGkU/2g6TKFl1LNJcdu31W8ksG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lN5HI/btsCyLrPGkU/2g6TKFl1LNJcdu31W8ksG0/img.png&quot; data-alt=&quot;enabled&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lN5HI/btsCyLrPGkU/2g6TKFl1LNJcdu31W8ksG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlN5HI%2FbtsCyLrPGkU%2F2g6TKFl1LNJcdu31W8ksG0%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;1232&quot; height=&quot;268&quot; data-filename=&quot;스크린샷 2023-11-11 오후 4.42.42.png&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;enabled&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. JobParameter 지원 범위 확대&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-25 오전 12.26.13.png&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;948&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBMagb/btsCy7nZaHB/4Av57Su5Awd31UKj2YoGTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBMagb/btsCy7nZaHB/4Av57Su5Awd31UKj2YoGTk/img.png&quot; data-alt=&quot;library&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBMagb/btsCy7nZaHB/4Av57Su5Awd31UKj2YoGTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBMagb%2FbtsCy7nZaHB%2F4Av57Su5Awd31UKj2YoGTk%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;322&quot; height=&quot;446&quot; data-filename=&quot;스크린샷 2023-12-25 오전 12.26.13.png&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;library&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v4 에서의 스프링 배치는 Job parameter 로&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;Long,&amp;nbsp;String,&amp;nbsp;Date,&amp;nbsp;Double&lt;/span&gt; 만 사용이 가능했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v5 에서는 여기에 더해 converter 를 직접 구현하는 것으로 모든 종류의 타입을 JobParameter 로 사용할 수 있도록 개선되었습니다. 또한 내장으로&amp;nbsp;LocalDateTime부터 LocalDate, Time까지 Converter가 들어가있으니 그냥 사용하기만 하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q1. chunk size 는 왜 job Parameter로 받지 않을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 안받는게 아니라 못받는다. (제가 테스트 해봤을땐 그랬습니다..)&lt;/p&gt;
&lt;pre id=&quot;code_1704644515057&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@JobScope
public Step step1() {
        return new StepBuilder(&quot;step1&quot;, jobRepository)
        .&amp;lt;PointsStatistics, PointsStatistics&amp;gt;chunk(jobParameter.getChunkSize(), platformTransactionManager)
        .reader(reader())
        .writer(writer())
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 job param을 통해서 chunk size를 런타임에 주입하면 null이 들어왔다고 에러가 나온다. 실제 디버깅을 해보면 아래의 BeanCreateionException 에러 메시지가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-18 오후 11.42.16.png&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUo0DU/btsC5YjWz70/WcbsW7YBVwWAXYiCyYK0N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUo0DU/btsC5YjWz70/WcbsW7YBVwWAXYiCyYK0N1/img.png&quot; data-alt=&quot;BCE&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUo0DU/btsC5YjWz70/WcbsW7YBVwWAXYiCyYK0N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUo0DU%2FbtsC5YjWz70%2FWcbsW7YBVwWAXYiCyYK0N1%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;1752&quot; height=&quot;156&quot; data-filename=&quot;스크린샷 2023-11-18 오후 11.42.16.png&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;BCE&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepBuilder의 구성을 보면 chunkSize를 입력해야하는데 이때, null이 들어간 상태로 만들어지기때문에 정상적으로 step빈이 만들어지지 않는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-18 오후 9.04.34.png&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AKhcF/btsC7npZFKB/HG9Z7nXnNev40SQXAWzkLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AKhcF/btsC7npZFKB/HG9Z7nXnNev40SQXAWzkLK/img.png&quot; data-alt=&quot;step Builder&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AKhcF/btsC7npZFKB/HG9Z7nXnNev40SQXAWzkLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAKhcF%2FbtsC7npZFKB%2FHG9Z7nXnNev40SQXAWzkLK%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;1620&quot; height=&quot;954&quot; data-filename=&quot;스크린샷 2023-11-18 오후 9.04.34.png&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;step Builder&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;b&gt;Step빈이 생성되는 시점이 jobParam의 late binding시점에 생성되도록 하면 더 편하지 않을까? &lt;/b&gt;라는 의문을 갖게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 글을 보고 이해를 했다. 해석하자면 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I mentioned that the chunk size could be made dynamic through application/system properties or job parameters. I deliberately put application/system properties first because I would recommend this way to configure such &quot;technical&quot; properties (chunk-size, thread-pool size, database connection pool size, etc). I would use job parameters for &quot;business&quot; properties like input file name, input table name, etc.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;job param이외에 application/system properties도 받을 수 있도록 의도적으로 두었다고한다. 이 이유가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;technical&quot; properties( chunk-size, thread-pool size, database connection pool size, etc)은 job param으로 받지 않고, &quot;business&quot; properties like input file name, input table name, etc. 와 같은 것들을 job param으로 받도록 하기 위함이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 chunk사이즈를 설정한 후에 job Param이 생성되고 이후부터 jobParam이 주입되어 사용될 수 있다. 그러므로 chunkSize를 설정할 때, jobParam을 주입할 수 없다. (하지만 편볍으로 설정할수 있다곤 한다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출처: &lt;a href=&quot;https://github.com/spring-projects/spring-batch/issues/4134#issuecomment-1164256892&quot;&gt;Dynamic Commit interval support through StepExecution class. &amp;middot; Issue #4134 &amp;middot; spring-projects/spring-batch&lt;/a&gt; &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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98/dashboard&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2695/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/2695/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/job/running.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-batch/reference/job/running.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>CURSOR</category>
      <category>job</category>
      <category>Paging</category>
      <category>Spring Batch 5</category>
      <category>step</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/160</guid>
      <comments>https://giron.tistory.com/160#entry160comment</comments>
      <pubDate>Mon, 8 Jan 2024 02:01:44 +0900</pubDate>
    </item>
    <item>
      <title>[오브젝트] 6장 메시지와 인터페이스</title>
      <link>https://giron.tistory.com/159</link>
      <description>&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;p data-ke-size=&quot;size16&quot;&gt;유연하고 재사용 가능한 퍼블릭 인터페이스를 만들기 위한 원칙과 기법을 살펴보는 것이 주제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;협력&lt;/b&gt;과 &lt;b&gt;메시지&lt;/b&gt;에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협력 관계를 설명하는 전통적인 메타포는 &lt;b&gt;클라이언트-서버&lt;/b&gt; 모델이다. &lt;u&gt;협력&lt;/u&gt;은 클라이언트가 서버의 서비스를 요청하는 &lt;u&gt;단방향 상호작용&lt;/u&gt;이다. 그리고 객체는 클라이언트와 서버의 역할을 동시에 수행한다.&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;다른 하나는 외부의 객체에게 전송하는 메시지의 집합이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협력에 적합한 객체를 설계하기 위해서는 외부에 전&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;송하는 메시지의 집합도 함께 고려하는 것이 바람직하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;b&gt;메시지 전송&lt;/b&gt; 또는 &lt;b&gt;메시지 패싱&lt;/b&gt;이라고 부른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 전송하는 객체를 &lt;u&gt;메시지 전송자&lt;/u&gt;라고 부르고&lt;/li&gt;
&lt;li&gt;메시지를 수신하는 객체를 &lt;u&gt;메시지 수신자&lt;/u&gt;라고 부른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 서버 관점에서는 &lt;b&gt;전송자는 클라이언트&lt;/b&gt;, &lt;b&gt;수신자는 서버&lt;/b&gt;라고 부르기도 한다. 메시지는 오퍼레이션명과 인자로 구성되며 메시지 전송은 메시지 수신자를 추가한 것이다.&lt;/p&gt;
&lt;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;메시지 전송 표기법 with Java&lt;/h4&gt;
&lt;pre id=&quot;code_1702791727146&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;condition.isSatisfiedBy(screening);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;condition: 수신자&lt;/li&gt;
&lt;li&gt;isSatisfiedBy: 오퍼레이션명&lt;/li&gt;
&lt;li&gt;screening: 인자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 코드가 실행되는지는 메시지 수신자의 실제 타입에 달려있다. condition은 DiscountCondition이라는 인터페이스 타입으로 정의돼 있지만 실제 수행 코드는 인터페이스를 실체화한 클래스 종류에 따라서 달라진다.&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;메시지와 메서드의 구분은 메시지 전송다와 수신자가 느슨하게 결합될 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션이라고 부른다. 오퍼레이션은 수행가능한 어떤 행동에 대한 추상화다. 오퍼레이션을 부를때는 내부 코드는 제외하고 단순히 메시지와 관련된 시그니처를 가리키는 경우가 대부분이다. 그에 반해 메시지를 수신했을 때 실제로 실행되는 코드는 메서드라고 부른다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-06 오후 11.27.48.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pKddS/btsB5YMd3BE/GrRmY5PEIHYLJvcaIXKumk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pKddS/btsB5YMd3BE/GrRmY5PEIHYLJvcaIXKumk/img.png&quot; data-alt=&quot;예시 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pKddS/btsB5YMd3BE/GrRmY5PEIHYLJvcaIXKumk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpKddS%2FbtsB5YMd3BE%2FGrRmY5PEIHYLJvcaIXKumk%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;1376&quot; height=&quot;546&quot; data-filename=&quot;스크린샷 2023-12-06 오후 11.27.48.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 오퍼레이션에 다양한 메서드를 구성해서 다형성을 활용하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 인터페이스와 설계 품질&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 인터페이스는 최소한의 꼭 필요한 오퍼레이션만을 인터페이스에 포함한다. 추상적인 인터페이스는 어떻게 수행하는지가 아니라 무엇을 하는지를 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 인터페이스를 설계하는 방법은 책임 주도 설계 방법을 따르는 것이다. 이 방법은 메시지를 먼저 선택함으로써 협력과 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다. (인터페이스 분리 원칙)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 메시지를 선택하는 것이 아니라, 메시지가 객체를 선택하게 함으로써 클라이언트의 의도를 메시지에 표현할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원칙과 기법 소개&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디미터 법칙
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라&lt;/li&gt;
&lt;li&gt;이웃하고만 얘기해라, 낯선 자에게 말하지 말라&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;묻지 말고 시켜라&lt;/li&gt;
&lt;li&gt;의도를 드러내는 인터페이스&lt;/li&gt;
&lt;li&gt;명령-쿼리 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;디미터의 법칙&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스가 특정 조건을 만족하는 대상에게만 메시지를 전송하도록 해야한다. 모든 클래스 C와 그의 메서드를 M으로 가정할때, M이 메시지를 전송할 수 있는 모든 객체는 다음 서술된 클래스의 인스턴스여야 한다. 이때 M에 의해 생성된 객체나 M이 호출하는 메서드에 의해 생성된 객체, 전역 변수로 선언된 객체는 모두 M의 인자로 간주한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;M의 인자로 전달된 클래스(자신 포함)&lt;/li&gt;
&lt;li&gt;C의 인스턴스 변수의 클래스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉬운 예시로 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍 해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;this 객체&lt;/li&gt;
&lt;li&gt;메서드의 매개변수&lt;/li&gt;
&lt;li&gt;this의 속성&lt;/li&gt;
&lt;li&gt;this의 속성인 컬렉션의 요소&lt;/li&gt;
&lt;li&gt;메서드 내에서 생성된 지역 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기차충돌 : screening.getMovie().getDiscountConditions()와 같이 만들지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디미터의 법칙은 내부 구조를 묻는 메시지가 아니라 수신자에게 무언가를 시키는 메시지가 더 좋은 메시지라고 속삭인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서는 ReservationAgency가 Movie, DiscountCondition을 알고있어서 강한 결합 및 메시지를 보내지 않고 있었는데 디미터의 법칙을 적용해서 이런 결합도를 낮추고 유지보수하기 좋은 코드가 되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;묻지말고 시켜라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향의 기본은 함께 변경될 확률이 높은 정보와 행동을 하나의 단위로 통합하는 것이다. 내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재한다면 객체가 책임져야하는 어떤 행동이 객체 외부로 누수된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) if(screening.getPosition() &amp;ne; &amp;ldquo;1st room&amp;rdquo;) &amp;larr; 누수된 예시&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의도를 드러내는 인터페이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 이름 짓는 방법&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메서드가 작업을 어떻게 수행하는지를 나타내도록 짓는 것 &amp;rarr; X&lt;/li&gt;
&lt;li&gt;어떻게가 아니라 &lt;b&gt;무엇을&lt;/b&gt; 하는지를 드러내는 것이다. &amp;rarr; O&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안좋은 메서드 네이밍 예시&lt;/p&gt;
&lt;pre id=&quot;code_1702791906061&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PeriodCondition {
	public boolean isSatisfiedByPeriod(Screening screening) {...}
}

public class SequenceCondition {
	public boolean isSatisfiedBySequence(Screening screening) {...}
}&lt;/code&gt;&lt;/pre&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;메서드 수준의 캡슐화를 위반한다는 것이다. PeriodCondition을 사용하는 코드를 SequenceCondition으로 바꾸면 메서드또한 바꿔줘야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 2번의 방식인 무엇을 하는지를 드러내도록 작성하는 것이 좋다. 또한 1번 방식은 이른 시기부터 클래스 내부 구현에 관한 고민이 발생할수 있다. 반면에 2번은 외부의 객체가 메시지를 전송하는 &lt;u&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;&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;결국엔 코드를 수정하려면 내부 로직을 읽어봐야하기 때문이다. 2번의 명확한 장점은 코드를 읽을때 어떻게 메시지를 보내는 것이 아닌 &lt;u&gt;무엇을&lt;/u&gt; 보냈는지에만 집중하면 된다. &lt;u&gt;어떻게&lt;/u&gt; 메시지를 보내야할지 파악하는것은 실제 리펙토링할때 코드를 보고 확인하는게 맞다고 생각한다.&lt;/p&gt;
&lt;pre id=&quot;code_1702791934283&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PeriodCondition {
	public boolean isSatisfiedBy(Screening screening) {...}
}

public class SequenceCondition {
	public boolean isSatisfiedBy(Screening screening) {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 클라이언트가 두 메서드를 가진 객체를 동일한 타입으로 간주할 수 있도록 동일한 타입 계층으로 묶어야 한다. 인터페이스를 사용하여 적용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1702791954170&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PeriodCondition implements DiscountCondition{
	public boolean isSatisfiedBy(Screening screening) {...}
}

public class SequenceCondition implements DiscountCondition{
	public boolean isSatisfiedBy(Screening screening) {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇에 집중한 패턴을 &lt;b&gt;의도를 드러내는 선택자&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원칙의 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원칙이 현재상황에 부적합하다고 판단되면 과감하게 원칙을 무시해라. 그것이 초보자와 고수의 차이이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고려할만한 이슈&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1702791974159&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;IntStream.of(1, 15, 20, 3, 9).filter(x -&amp;gt; x &amp;gt; 10).distinct().count()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 디미터의 법칙을 위반하지않는다. 디미터의 법칙은 결합도와 관련된 것이며, 결합도가 문제 되는 것은 객체의 내부 구조가 외부로 노출되는 경우로 한정된다. IntStream을 다른 IntStream으로 변환할 뿐, 객체를 둘러싸고 있는 캡슐은 그대로 유지된다. 즉,&lt;u&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;&lt;b&gt;결합도와 응집도의 충돌&lt;/b&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;Theater클래스에서 Audience객체의 getBag()을 통해서 구현되어있어, Theater가 Audienece의 내부 구조에 강하게 결합되어 있다. 이를 해결하기위해 질문하고, 판단하고, 상태를 변경하는 코드를 Audience로 옮기는 것이다. 이러면 Audience가 상태와 행동을 조작하므로 응집도가 높아졌다.&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;Before&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1702792049271&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PeriodCondition implements DiscountCondition {
	public boolean isSatisifiedBy(Screening screening){
		return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &amp;amp;&amp;amp;
			startTime.compareTo(screening.getStartTime().toLocalTime()) ...;
}&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;After&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1702792076985&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Screening {
    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime startTime,LocalTime endTime){
        return whenScreened.getDayOfWeek().equals(dayOfWeek) &amp;amp;&amp;amp;
                startTime.compareTo(whenScreened.toLocalTime()) &amp;lt;= 0 ...
    }
}

public class PeriodCondition implements DiscountCondition {
    public boolean isSatisfiedBy(Screening screening){
        return screening.isDiscountable(dayOfWeek, startTime, endTime);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;screening이 기간에 따른 할인 조건을 판단하는 책임을 떠안게 된다. 이것은 책임의 본질이 아니다. Screening이 직접 할인 조건을 판단하게 되면 객체의 응집도가 낮아진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;명령-쿼리 분리 원칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절차를 묶어 호출 가능하도록 이름을 부여하는 기능 모듈을 루틴이라고 부른다. 루틴은 프로시저와 함수로 구분된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저는 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류이다.(반환 x)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류이다.(상태 변경 x)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명령&lt;/b&gt;과 &lt;b&gt;쿼리&lt;/b&gt;는 객체의 인터페이스 측면에서 &lt;b&gt;프로시저&lt;/b&gt;와 &lt;b&gt;함수&lt;/b&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;명령과 쿼리를 분리해야 하는 이유는 실행 결과를 예측하기 쉽게 하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인의 두 가지 중요한 용어인 &lt;b&gt;이벤트와 반복 일정이 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트는 특정 일자에 실제로 발생하는 사건을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 일정은 일주일 단위로 돌아오는 특정 시간 간격에 발생하는 사건 전체를 포괄적으로 지칭하는 용어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트가 발생하고 반복 일정에 포함되는지 확인하는 메서드가 있다고 가정하자. 이벤트에서 isSatisfiedBy 메서드를 호출하고 만족하면 스케줄을 변경하는 로직(reschedule)이 내부에 있다고 가정하면, 메서드를 두번 실행하면 값이 매번 바뀔 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만족하는지만 확인할 것같은 로직에서 정보 수정이 있다면 예측하기 어려워진 로직이 될것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령-쿼리 분리 원칙에 따라 수정하면 isSatisfied메서드는 부수효과를 가지지 않는 반환만 하는 메서드가 되고 reschedule 메서드는 반환 값을 돌려주지 않고 상태만 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령과 쿼리를 분리함으로써 명령형 언어의 틀 안에서 참조 투명성의 장점을 제한적으로나마 누릴 수 있다. 참조 투명성이란 &amp;ldquo;어떤 표현식 e가 있을때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 어떤 표현식 e가 있을때 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) f(1) + f(1) = 6, f(1) * 2 = 6일때, f(1)을 3으로 교체하더라도 결과가 같아지는 특성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유는 불변성때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향프로그래밍은 객체의 상태 변경이라는 부수효과를 기반으로 하기 때문에 참조 투명성은 예외에 가깝다. 명령-쿼리 분리 원칙으로 부수효과를 가지는 명령으로부터 부수효과가 없는 쿼리를 분리함으로써 제한적이나마 참조 투명성의 혜택을 누릴수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책임 주도 설계에서는 객체가 메시지를 선택하는 것이 아닌 메시지가 객체를 선택하기 때문에 협력에 적합한 메시지를 결정할 확률이 높아진다. 협력에 적합한 객체가 아닌 메시지이다.&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>6장</category>
      <category>객체지향</category>
      <category>디미터의 법칙</category>
      <category>명령-쿼리</category>
      <category>오브젝트</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/159</guid>
      <comments>https://giron.tistory.com/159#entry159comment</comments>
      <pubDate>Sun, 17 Dec 2023 14:55:06 +0900</pubDate>
    </item>
    <item>
      <title>rpc(GRPC) vs HTTP</title>
      <link>https://giron.tistory.com/158</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;RPC&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 프로시저 호출(Remote Procedure Call)은 네트워크의 세부 사항을 이해하지 않고도 한 프로그램이 네트워크의 다른 컴퓨터에 있는 프로그램에 서비스를 요청하는 데 사용할 수 있는 소프트웨어 통신 프로토콜이다.&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;RPC는&amp;nbsp;클라이언트 - 서버 모델이다.&amp;nbsp;요청하는 프로그램은 클라이언트이고, 서비스를 제공하는 프로그램은 서버이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 프로시저 호출과 마찬가지로 RPC는&amp;nbsp;&amp;nbsp;원격 프로시저의 결과가 반환될 때까지 요청 프로그램을 일시 중지해야 하는&amp;nbsp;동기 작업이다.&amp;nbsp;그러나 동일한 주소 공간을 공유하는 경량 프로세스 또는&amp;nbsp;스레드를&amp;nbsp;사용하면&amp;nbsp; 여러 RPC를 동시에 수행할 수 있다.(+ grpc를 사용하면 비동기식으로 사용할 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 설명하는 데 사용되는 사양 언어인 &lt;b&gt;IDL&lt;/b&gt;(인터페이스 정의 언어)은 원격 프로시저 호출 소프트웨어에서 일반적으로 사용된다. 이 경우 IDL은 링크의 양쪽 끝에 있는 서로 다른 운영 체제(OS)와 컴퓨터 언어를 사용하는 컴퓨터 사이에 중간다리 역할을 제공한다.&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;RPC stub&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC 프레임워크를 사용하는 프로그램이 &lt;u&gt;컴파일되면&amp;nbsp;&lt;/u&gt;원격 프로시저 코드를 나타내는 역할을 하는 &lt;u&gt;컴파일된 코드에&amp;nbsp;stub이 포함된다.&lt;/u&gt;&amp;nbsp;프로그램이 실행되고 프로시저 호출이 실행되면 stub 코드는 요청을 수신하여 이를&amp;nbsp;로컬 컴퓨터의 클라이언트&amp;nbsp;런타임 프로그램에 전달한다.&amp;nbsp;클라이언트 stub이 처음 호출되면 nameserver에 접속하여 서버가 있는 전송 주소를 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 런타임 프로그램은 원격 컴퓨터와 서버 애플리케이션의 주소를 지정하는 방법을 알고 있으며 원격 프로시저를 요청하는 메시지를 네트워크를 통해 보낸다.&amp;nbsp;마찬가지로 서버에는 원격 프로시저 자체와 인터페이스하는 런타임 프로그램과 스텁이 포함되어 있다.&amp;nbsp;응답 요청 프로토콜은 동일한 방식으로 반환된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RPC 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 프로시저가 호출되면 호출 환경이 일시 중단되고 프로시저 매개변수가 네트워크를 통해 프로시저가 실행될 환경으로 전송된 다음 해당 환경에서 프로시저가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저가 완료되면 결과가 호출 환경으로 다시 전송되며, 여기서 일반 프로시저 호출에서 반환되는 것처럼 실행이 재개된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트는 클라이언트 stub을 호출한다.&lt;/li&gt;
&lt;li&gt;클라이언트 스텁은 프로시저 매개변수를 메시지로 압축하고 시스템 호출을 통해 메시지를 보냅니다. 프로시저 매개변수를 패킹하는 것을&amp;nbsp;&lt;a href=&quot;https://www.techtarget.com/whatis/definition/marshalling&quot;&gt;마샬링&lt;/a&gt;이라고 한다.&lt;/li&gt;
&lt;li&gt;클라이언트의 로컬 OS는 클라이언트 시스템에서 원격 서버 시스템으로 메시지를 보낸다.&lt;/li&gt;
&lt;li&gt;서버 OS는 들어오는 패킷을 서버 스텁에 전달한다.&lt;/li&gt;
&lt;li&gt;서버 스텁은&amp;nbsp;메시지에서 언&amp;nbsp;마샬링(unmarshalling )이라고 하는 매개변수를 압축 해제한다.&lt;/li&gt;
&lt;li&gt;서버 프로시저가 완료되면 서버 스텁으로 돌아가서 반환 값을 메시지로 마샬링한다.&amp;nbsp;그런 다음 서버 스텁은 메시지를 전송 계층으로 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전송 계층&lt;/b&gt;은 결과 메시지를 클라이언트 전송 계층으로 다시 보내고, 클라이언트 &lt;b&gt;전송 계층&lt;/b&gt;은 해당 메시지를 클라이언트 스텁으로 다시 전달한다.&lt;/li&gt;
&lt;li&gt;클라이언트 스텁은 반환 매개변수를 정렬 해제하고 실행은 호출자에게 반환됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FxuQr/btsAM5YK4o4/bRETuAVCtcZvQ1TRKgkxGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FxuQr/btsAM5YK4o4/bRETuAVCtcZvQ1TRKgkxGK/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FxuQr/btsAM5YK4o4/bRETuAVCtcZvQ1TRKgkxGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFxuQr%2FbtsAM5YK4o4%2FbRETuAVCtcZvQ1TRKgkxGK%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;524&quot; height=&quot;600&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gRPC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rpc의 프레임워크인 gRPC는 구글이 최초로 개발한 오픈 소스 원격 프로시저 호출 시스템이다. 전송을 위해 HTTP/2를, 인터페이스 정의 언어로 프로토콜 버퍼를 사용하며 인증, 양방향 스트리밍 및 흐름 제어, 차단 및 비차단 바인딩, 취소 및 타임아웃 등의 기능을 제공한다. - 위키백과-&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP2, 프로토콜 버퍼가 핵심&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로토콜 버퍼&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 작고 빠르며 기본 언어 바인딩을 생성한다는 점을 제외하면 JSON과 같다.&amp;nbsp;데이터를 구조화하는 방법을 &lt;b&gt;한 번 정의하면&lt;/b&gt; 특별히 생성된 소스 코드를 사용하여 다양한 데이터 스트림에서 다양한 언어를 사용하여 구조화된 데이터를 쉽게 쓰고 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토콜 버퍼는 정의 언어(&amp;nbsp;.proto파일로 생성됨), proto 컴파일러가 데이터와 인터페이스하기 위해 생성하는 코드이다. 파일에 기록되거나 네트워크를 통해 전송되는 데이터의 직렬화 형식의 조합이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토콜 버퍼는 언어 중립적이고 플랫폼 중립적이며 확장 가능한 방식으로 구조화된 레코드 형식의 데이터를 직렬화해야 하는 모든 상황에 이상적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토콜 버퍼를 사용하면 다음과 같은 이점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴팩트한 데이터 저장&lt;/li&gt;
&lt;li&gt;빠른 구문 분석&lt;/li&gt;
&lt;li&gt;다양한 프로그래밍 언어에서의 가용성&lt;/li&gt;
&lt;li&gt;자동 생성된 클래스를 통한 기능 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로토콜 버퍼는 전체 메시지가 한 번에 메모리에 로드될 수 있고 개체 그래프보다 크지 않다고 가정하는 경향이 있다. 따라서 몇 메가바이트를 초과하는 데이터의 경우 다른 솔루션을 고려하자.&lt;/li&gt;
&lt;li&gt;프로토콜 버퍼가 직렬화되면 동일한 데이터가 다양한 이진 직렬화를 가질 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;채널&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC 채널은 지정된 호스트 및 포트에서 gRPC 서버에 대한 연결을 제공한다.&amp;nbsp;클라이언트 스텁을 생성할 때 사용된다.&amp;nbsp;클라이언트는 메시지 압축을 켜거나 끄는 것과 같은 gRPC의 기본 동작을 수정하기 위해 채널 인수를 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널에는&amp;nbsp;connected및 idle등 의 상태가 있으며 virtual connection이 가능하다. 클라이언트가 채널을 만들면 내부적으로 http2 통신을 한다. gRPC가 채널 폐쇄를 처리하는 방법은 언어에 따라 다르며 채널에는 많은 rpc가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션이 닫혀도 채널이 열려있으면 커넥션이 다시 맺어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC 클라이언트는 리졸버와 LB를 가지고 있다. 리졸버는 주기적으로 DNS를 리졸브하면서 앤드포인트를 갱신한다. 커넥션이 실패하면 LB는 직전에 사용했던 address list를 사용하여 재연결 시작한다. 커넥션 풀을 관리한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-31 오전 12.06.29.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLaYxp/btsAIceyfVf/TicWNSPfCrUugr7kVKnETK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLaYxp/btsAIceyfVf/TicWNSPfCrUugr7kVKnETK/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLaYxp/btsAIceyfVf/TicWNSPfCrUugr7kVKnETK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLaYxp%2FbtsAIceyfVf%2FTicWNSPfCrUugr7kVKnETK%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;1308&quot; height=&quot;284&quot; data-filename=&quot;스크린샷 2023-10-31 오전 12.06.29.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gRPC 요청과 응답&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 메시지는 응답 헤더, length prefixed Message, Trailer 3가지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trailer 헤더는 http 스펙으로 grpc-status와 grpc-message를 포함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-31 오전 12.13.02.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FGvaG/btsAMoYBMDE/3gUQjwqCuicYW9SX71tPpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FGvaG/btsAMoYBMDE/3gUQjwqCuicYW9SX71tPpK/img.png&quot; data-alt=&quot;헤더&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FGvaG/btsAMoYBMDE/3gUQjwqCuicYW9SX71tPpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFGvaG%2FbtsAMoYBMDE%2F3gUQjwqCuicYW9SX71tPpK%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;1532&quot; height=&quot;388&quot; data-filename=&quot;스크린샷 2023-10-31 오전 12.13.02.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;헤더&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;왜 gRPC에 특별한 로드밸런싱이 필요한가?(쿠버네티스 환경에서)&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(쿠버네티스의 service는 L4, Ingress는 L7 로드밸런싱을 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 HTTP/2로 구축되었고, HTTP/2는 하나의 오래 지속되는 TCP 연결을 갖도록 설계되있기 때문에, 모든 요청은&amp;nbsp;다중화(multiplexed)(특정 시점에 다수의 요청이 하나의 연결에서만 동작하는 것을 의미)된다. 일반적으로, 그것은 연결 관리 오버헤드를 줄이는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 그것은 또한 연결 수준(L4)의 밸런싱(balancing)에는 유용하지 않다는 것을 의미한다. 일단 연결이 설정되면, 더 이상 밸런싱을 수행할 수 없기 때문이다. 모든 요청이 아래와 같이 단일 파드에 고정될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-31 오전 1.04.15.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmZnxy/btsAMU32WMx/Om8XlyKlXRp7ZhRHXtHQPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmZnxy/btsAMU32WMx/Om8XlyKlXRp7ZhRHXtHQPK/img.png&quot; data-alt=&quot;L4&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmZnxy/btsAMU32WMx/Om8XlyKlXRp7ZhRHXtHQPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmZnxy%2FbtsAMU32WMx%2FOm8XlyKlXRp7ZhRHXtHQPK%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;527&quot; height=&quot;479&quot; data-filename=&quot;스크린샷 2023-10-31 오전 1.04.15.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;L4&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP/1.1에서는 발생하지 않는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2와 달리 HTTP/1.1은 요청을 다중화할 수 없다. TCP 연결 시점에 하나의 HTTP 요청만 활성화될 수 있다. 예를 들어 클라이언트가 'GET /foo'를 요청하고, 서버가 응답할 때까지 대기한다. 요청-응답 주기가 발생하면, 해당 연결에서 다른 요청을 실행할 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gRPC with LB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC 로드 밸런싱을 수행하려면, 연결 밸런싱에서&amp;nbsp;요청&amp;nbsp;밸런싱으로 전환해야 한다. 즉, 각각에 대한 HTTP/2 연결을 열어야 하고, 아래와 같이, 이러한 연결들로&amp;nbsp;요청의 밸런싱을 맞춘다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-31 오전 1.08.00.png&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rhz6Z/btsAJ9uDlXd/tvXY29wFvcwOtgABxTUzFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rhz6Z/btsAJ9uDlXd/tvXY29wFvcwOtgABxTUzFK/img.png&quot; data-alt=&quot;LB&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rhz6Z/btsAJ9uDlXd/tvXY29wFvcwOtgABxTUzFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frhz6Z%2FbtsAJ9uDlXd%2FtvXY29wFvcwOtgABxTUzFK%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;416&quot; height=&quot;373&quot; data-filename=&quot;스크린샷 2023-10-31 오전 1.08.00.png&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LB&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 측면에서, L3/L4에서 결정을 내리기 보다는 L5/L7에서 결정을 내려야 한다. 즉, TCP 연결을 통해 전송된 프로토콜을 이해해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션 코드는 대상에 대한 자체 로드 밸런싱 풀을 수동으로 유지 관리할 수 있고, gRPC 클라이언트에&amp;nbsp;&lt;a href=&quot;https://godoc.org/google.golang.org/grpc/balancer&quot;&gt;로드 밸런싱 풀을 사용&lt;/a&gt;하도록 구성할 수 있다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커스텀하므로 높은 제어력을 가지지만, 파드가 리스케줄링(reschedule)되면서 풀이 변경되는 쿠버네티스와 같은 환경에서는 매우 복잡&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;쿠버네티스에서 앱을&amp;nbsp;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/services-networking/service/#%ED%97%A4%EB%93%9C%EB%A6%AC%EC%8A%A4-headless-%EC%84%9C%EB%B9%84%EC%8A%A4&quot;&gt;헤드리스(headless) 서비스&lt;/a&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;/ol&gt;
&lt;/li&gt;
&lt;li&gt;경량 프록시를 사용
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://linkerd.io/&quot;&gt;linkerd&lt;/a&gt; 나 istio처럼 서비스 메시를 사용한다.&lt;/li&gt;
&lt;li&gt;각 파드에 작고, 초고속인 프록시를 추가하는 것을 의미하며, 이러한 프록시가 쿠버네티스 API를 와치(watch)하고 gRPC 로드 밸런싱을 자동으로 수행하는 것을 의미이다&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST API vs gRPC&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;통신 모델
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;REST API를 사용할 때 클라이언트는 단일 REST API 요청을 서버에 전송한다.&lt;/li&gt;
&lt;li&gt;하지만 gRPC는 여러 서버에 동시에 요청이 가능하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;서버에서 직접적으로 호출 가능한 작업
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;REST에는 URL로 정의된 서버 리소스에 대해 클라이언트에서 사용할 수 있는 제한된 HTTP 요청 동사 세트가 있다. 클라이언트는 리소스 자체를 직접적으로 호출한다. 이를&amp;nbsp;엔터티 지향 설계라고 하며 객체 지향 프로그래밍 방법과 잘 맞다.&lt;/li&gt;
&lt;li&gt;gRPC API에서 직접적으로 호출 가능한 서버 작업은 함수 또는 프로시저라고도 하는 서비스에 의해 정의된다. gRPC 클라이언트는 애플리케이션 내에서 함수를 직접적으로 호출하는 것처럼 간접적으로 호출한다. 이를&amp;nbsp;서비스 지향 설계라고 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;데이터 교환 형식
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;REST API를 사용할 때는 소프트웨어 구성 요소 간에 전달되는 데이터 구조가 일반적으로 JSON 데이터 교환 형식으로 표현된다. XML 및 HTML과 같은 다른 데이터 형식을 전달하는 것도 가능하다. JSON은 읽기 쉽고 유연하지만 직렬화해야 하고 프로그래밍 언어로 번역해야 한다.&lt;/li&gt;
&lt;li&gt;반대로 gRPC는 기본적으로 Protocol Buffer(Protobuf) 형식을 사용하지만 기본 JSON 지원한다. 서버는 &lt;b&gt;프로토타입 사양 파일&lt;/b&gt;에서 Protocol Buffer 인터페이스 설명 언어(IDL)를 사용하여 데이터 구조를 정의한다. gRPC는 구조를 바이너리 형식으로 직렬화한 다음 지정된 프로그래밍 언어로 역직렬화한다. 이 메커니즘은 전송 중에 압축되지 않는 JSON을 사용하는 것보다 더 빠르다.&lt;/li&gt;
&lt;/ol&gt;
gRPC는 긴밀하게 결합되기 때문에 클라이언트와 서버에서 동일한 proto 파일에 액세스할 수 있어야 한다. 파일을 업데이트하려면 서버와 클라이언트 모두에서 업데이트가 필요하다.(커플링)&lt;/li&gt;
&lt;li&gt;따라서 gRPC는 시간이 지나도 API가 변경될 가능성이 낮은, 여러 프로그래밍 언어로 구성된 마이크로서비스 아키텍처에도 적합하다. 또한 gRPC는 작동하려면 클라이언트 측과 서버 측 모두에 gRPC 소프트웨어가 필요하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://grpc.io/docs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://grpc.io/docs/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://protobuf.dev/overview/&quot;&gt;Overview&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kubernetes.io/ko/blog/2018/11/07/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EC%97%90%EC%84%9C-%EC%96%B4%EB%A0%A4%EC%9B%80-%EC%97%86%EC%9D%B4-grpc-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1%ED%95%98%EA%B8%B0/&quot;&gt;쿠버네티스에서 어려움 없이 gRPC 로드밸런싱하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.techtarget.com/searchapparchitecture/definition/Remote-Procedure-Call-RPC&quot;&gt;What Is Remote Procedure Call (RPC)? Definition from SearchAppArchi...&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=igHrQPzLVRw&quot;&gt;당근마켓 gRPC 서비스 운영 노하우 | 당근마켓 SRE 밋업 1회&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=njC24ts24Pg&quot;&gt;gRPC in 5 minutes | Eric Anderson &amp;amp; Ivy Zhuang, Google&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/aspnet/core/grpc/comparison?view=aspnetcore-7.0&quot;&gt;gRPC 서비스와 HTTP API 비교&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>grpc</category>
      <category>HTTP</category>
      <category>MSA</category>
      <category>RPC</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/158</guid>
      <comments>https://giron.tistory.com/158#entry158comment</comments>
      <pubDate>Thu, 23 Nov 2023 00:10:32 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 사알짝 맛보기</title>
      <link>https://giron.tistory.com/157</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;본 게시글은 지속적으로 학습하면서 업데이트할 예정입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-22 오후 9.07.20.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G7WBT/btsyUKjzBH9/UaOPSbaiwPlfgpdKf4ij20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G7WBT/btsyUKjzBH9/UaOPSbaiwPlfgpdKf4ij20/img.png&quot; data-alt=&quot;구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G7WBT/btsyUKjzBH9/UaOPSbaiwPlfgpdKf4ij20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG7WBT%2FbtsyUKjzBH9%2FUaOPSbaiwPlfgpdKf4ij20%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;1666&quot; height=&quot;794&quot; data-filename=&quot;스크린샷 2023-10-22 오후 9.07.20.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작원리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;671&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/63cgr/btsy5gt0LGb/QQaESjKft5qnA1IySbfhw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/63cgr/btsy5gt0LGb/QQaESjKft5qnA1IySbfhw0/img.png&quot; data-alt=&quot;따배쿠&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/63cgr/btsy5gt0LGb/QQaESjKft5qnA1IySbfhw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F63cgr%2Fbtsy5gt0LGb%2FQQaESjKft5qnA1IySbfhw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;671&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;따배쿠&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컨테이너 빌드(nginx, payment, batch &amp;hellip;)&lt;/li&gt;
&lt;li&gt;사용자가 docker push &lt;a href=&quot;http://hub.example.com/nginx&quot;&gt;hub.example.com/nginx&lt;/a&gt; 와 같이 docker hub에 저장해둔다.(사내, &lt;a href=&quot;http://docker.com&quot;&gt;docker.com&lt;/a&gt; 등)&lt;/li&gt;
&lt;li&gt;이후, 쿠버네티스 명령어를 통해 저장해둔 컨테이너가 실행하도록 한다.(yaml or kubectl 명령어로 실행 가능)&lt;/li&gt;
&lt;li&gt;명령어를 실행하면, 마스터 노드(Control-plane)에 요청이 간다.&lt;/li&gt;
&lt;li&gt;마스터 노드는 API서버가 있어서 쿠버네티스관련 요청을 받는다.&lt;/li&gt;
&lt;li&gt;스케줄러에게 어떤 워커 노드에 nginx를 실행하면 좋을지 물어보면, 스케줄러가 워커 노드의 상태를 보고 적절한 노드를 응답해준다.(REST API서버에게)&lt;/li&gt;
&lt;li&gt;API서버는 할당받은 워커 노드 시스템에 있는 kubelet에 요청을 한다.(실행시켜달라고)&lt;/li&gt;
&lt;li&gt;워커 노드의 kubelet은 도커 명령어로 바꿔서 도커 데몬에게 컨테이너 실행 요청을 한다.&lt;/li&gt;
&lt;li&gt;도커 데몬은 도커 허브(&lt;a href=&quot;http://hube.example.com&quot;&gt;hube.example.com&lt;/a&gt;)에서 관련 컨테이너가 있는지(ex)nginx) 검색하고 있으면 컨테이너로 실행해준다.&lt;/li&gt;
&lt;li&gt;쿠버네티스는 이렇게 동작되는 컨테이너를 pod라는 단위로 관리하게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 컨테이너를 1)빌드하고 2)컨테이너를 저장하고 3)컨테이너를 k8s에서 실행했을때, 동작하는 과정이다.&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;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;etcd
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key-value 타입의 저장소&lt;/li&gt;
&lt;li&gt;워커 노드들에 대한 상태 정보&lt;/li&gt;
&lt;li&gt;kubelet이라는 데몬으로 (CAdvisor를 통해서) 정보를 수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;kube-api 서버
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;k8s API를 사용하도록 요청을 받고 요청이 유효한지 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;kube-scheduler
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파드를 실행할 노드 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;kube-controller-manager
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파드를 관찰하며 개수를 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;kubelet
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 노드에서 실행되는 k8s 에이전트&lt;/li&gt;
&lt;li&gt;데몬 형태로 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;kube-proxy
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;k8s의 network 동작을 관리&lt;/li&gt;
&lt;li&gt;iptables rule을 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;docker, containerd, runc&lt;/li&gt;
&lt;/ul&gt;
&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LpDAm/btsyVYPemXd/RDiVflSBa0UF0XeqDWzrKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LpDAm/btsyVYPemXd/RDiVflSBa0UF0XeqDWzrKK/img.png&quot; data-alt=&quot;따배쿠&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LpDAm/btsyVYPemXd/RDiVflSBa0UF0XeqDWzrKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLpDAm%2FbtsyVYPemXd%2FRDiVflSBa0UF0XeqDWzrKK%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;1274&quot; height=&quot;584&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;따배쿠&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;k8s namespace&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 하나를 여러개의 논리적인 단위로 나눠서 사용&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;a href=&quot;https://kubernetes.io/ko/docs/concepts/workloads/controllers/daemonset&quot;&gt;데몬셋&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/workloads/controllers/deployment/&quot;&gt;디플로이먼트&lt;/a&gt;&amp;nbsp;등)를 이용하여 클러스터 기능을 구현한다. 이들은 클러스터 단위의 기능을 제공하기 때문에 애드온에 대한 네임스페이스 리소스는&amp;nbsp;kube-system&amp;nbsp;네임스페이스에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 core-dns가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;coreDns&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 클러스터 내 POD에서 어떤 도메인을 찾고자 할 때&amp;nbsp;kube-system&amp;nbsp;네임스페이스에 실행되고 있는&amp;nbsp;&lt;b&gt;CoreDNS&lt;/b&gt;가 네임서버로 사용된다. 즉, 파드와 서비스를 위한 DNS 레코드를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreDNS도 POD로 실행되기 때문에 외부 요청을 받기 위해 Service 오브젝트가 존재한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kube-Proxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 네트워크 프록시는 각 노드에서 실행된다. 각 노드의 쿠버네티스 API에 정의된 서비스를 반영한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순한 TCP, UDP 및 SCTP 스트림 포워딩&lt;/li&gt;
&lt;li&gt;라운드 로빈 TCP, UDP 및 SCTP 포워딩을 백엔드 셋에서 수행 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Service&lt;/h2&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;각 파드는 고유한 IP 주소를 갖지만, 디플로이먼트에서는 한 시점에 실행되는 파드 집합이 잠시 후 실행되는 해당 파드 집합과 다를 수 있다. 이러한 부분을 신경쓰지않고 서비스로 추상화 시켜서 관리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Ingress&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP나 HTTPS를 통해 클러스터 내부의 서비스를 외부로 노출&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기능&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서비스에 외부 URL 제공&lt;/li&gt;
&lt;li&gt;트래픽을 로드밸런싱&lt;/li&gt;
&lt;li&gt;SSL 인증서 처리(Ingress에 인증서를 넣은 경우)&lt;/li&gt;
&lt;li&gt;virtual hosting 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스가 Ingress Controller를 만들어준다. &lt;b&gt;Ingress Rules&lt;/b&gt;을 만들어서 접속 url에 따라서 알맞은 Service에 연결해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) &lt;a href=&quot;http://example.com/&quot;&gt;example.com/&lt;/a&gt;auth &amp;rarr; auth service&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://example.com/order&quot;&gt;example.com/order&lt;/a&gt; &amp;rarr; order service&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;Ingreess rule&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;apiVersion: extensions/v1beta1
kind: Ingress
metadata:
	name: marvel-ingress # 1, 2이 동작해야 실행 가
spec:
	rules:
	-http:
		paths:
		-path: /
			backend:
				serviceName: marvel-service # 1
				servicePort:80
		-path: /pay
			backend:
				serviceName: pay-service # 2
				servicePort: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;웹서비스 동작&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl apply -f marvel-home.yaml -f pay.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl get deploy, replicationcontroller&lt;/li&gt;
&lt;li&gt;kubectl get svc&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ingress 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들어둔 yaml파일 실행하기 위해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl apply -f ingress.yaml&lt;/li&gt;
&lt;li&gt;kubectl get svc ingress-nginx-controller&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;1327&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BPlsT/btsy04Iba4d/sKzm2kcI4K8hUjAYudKxqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BPlsT/btsy04Iba4d/sKzm2kcI4K8hUjAYudKxqk/img.png&quot; data-alt=&quot;따배쿠&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BPlsT/btsy04Iba4d/sKzm2kcI4K8hUjAYudKxqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBPlsT%2Fbtsy04Iba4d%2FsKzm2kcI4K8hUjAYudKxqk%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;598&quot; height=&quot;388&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;1327&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;따배쿠&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ConfigMap&lt;/h2&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;value는 1MB초과 불가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key-value구조&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;pod에 일부분만 전달 가능&lt;/li&gt;
&lt;li&gt;pod에 전체 정보 전달 가능&lt;/li&gt;
&lt;li&gt;볼륨 마운트를 통해 특정 키를 파일처럼 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Helm&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스를 구성할 때, 사용되는 yaml들을 관리하는 패키지 매니저&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;차트를 이용하여 관리&lt;/li&gt;
&lt;li&gt;차트는 .tgz 형태의 file로 관리 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구성&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;Chart: helm 패키지
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;k8s application, tool, service를 구동하는데 필요한 resource 집합(nginx, redis 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Repository: helm chart를 모아두고 공유하는 저장소
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;helm repo add bitnami &lt;a href=&quot;https://charts.bitnami.com/bitnami&quot;&gt;https://charts.bitnami.com/bitnami&lt;/a&gt;&lt;a href=&quot;https://charts.bitnami.com/bitnami%E2%82%A9&quot;&gt;₩&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;helm repo list&lt;/li&gt;
&lt;li&gt;helm repo remove bitnami&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;명령어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;command 리소스가 존재하지 않을 경우 리소스가 이미 존재할 경우&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;create&lt;/td&gt;
&lt;td&gt;새로운 리소스가 생성됩니다.&lt;/td&gt;
&lt;td&gt;ERROR가 발생합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;apply&lt;/td&gt;
&lt;td&gt;새로운 리소스가 생성됩니다.&lt;/td&gt;
&lt;td&gt;리소스를 구성합니다.(부분적인 spec을 적용합니다.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;replace&lt;/td&gt;
&lt;td&gt;ERROR가 발생합니다.&lt;/td&gt;
&lt;td&gt;리소스가 삭제된 뒤 새롭게 생성됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create 명령어는 yaml 파일안에 모든 것을 기술해야 하는 반면 apply 명령어는 부분적인 spec만 주어져도 업데이트가 잘 진행 된다는 뜻입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파드 yaml을 통한 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubetcl create -f pod-nginx.yaml&lt;/li&gt;
&lt;li&gt;kubetcl create -f redis.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파드 종료(삭제)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl delete pod nginx-pod&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동작중인 파드 수정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl edit pod nginx-pod&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전체 namespace에서 검색
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl get pods &amp;mdash;all-namespaces&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;새로운 pod yaml만드는 방법( redis.yaml이름으로 redis라는 pod만들기)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl run redis &amp;mdash;image=redis123 &amp;mdash;dry-run -o yaml &amp;gt; redis.yaml&lt;/li&gt;
&lt;li&gt;dry run 은 실행하진 않고 실행 되는지만 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;imageError가 발생한다면?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl describe pod redis&lt;/li&gt;
&lt;li&gt;문제 확인후, kubectl edit pod redis&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=5sKkIg7k8nw&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=5sKkIg7k8nw&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/architecture/nodes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kubernetes.io/ko/docs/concepts/architecture/nodes/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/&quot;&gt;https://kubernetes.io/ko/docs/&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>Chart</category>
      <category>helm</category>
      <category>POD</category>
      <category>YAML</category>
      <category>쿠버네티스</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/157</guid>
      <comments>https://giron.tistory.com/157#entry157comment</comments>
      <pubDate>Mon, 23 Oct 2023 22:35:10 +0900</pubDate>
    </item>
    <item>
      <title>분산 시스템에서 데이터 처리(Queue, CDC, Outbox Pattern...)</title>
      <link>https://giron.tistory.com/156</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 분산 시스템에서 메시지를 주고받는 방법으로는 크게 2가지가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;API를 통한 통신
&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;간단한 개발&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&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;일반적으로 publisher가 데이터를 큐에 넣으면 consumer가 큐에서 데이터를 꺼내서 데이터를 가공한다.&lt;/li&gt;
&lt;li&gt;복잡한 개발&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서는 모든 데이터가 네트워크를 타면서 이동하므로 지연, 유실 등의 문제가 발생할 수 있다. 따라서 아래의 3가지 방식을 통해 데이터 전달을 보장하는 방법이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.43.48.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/binZUx/btsvkO90E8R/vfQd7ZEVY7DaPiEHBM9vD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/binZUx/btsvkO90E8R/vfQd7ZEVY7DaPiEHBM9vD0/img.png&quot; data-alt=&quot;데이터 전달 방법&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/binZUx/btsvkO90E8R/vfQd7ZEVY7DaPiEHBM9vD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbinZUx%2FbtsvkO90E8R%2FvfQd7ZEVY7DaPiEHBM9vD0%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;634&quot; height=&quot;295&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.43.48.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 전달 방법&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. At most once&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;producer가 최대 한 번만 송신하고 consumer가 최대 한 번만 수신한다.&lt;/li&gt;
&lt;li&gt;간단한 구현 &amp;amp; 개발이지만 데이터 유실 가능성이 높다.&lt;/li&gt;
&lt;li&gt;대용량 메세지 전송할 때 편하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. At least once&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;잘 받았다는 응답이 올때까지 메시지를 보낸다: Ack&lt;/li&gt;
&lt;li&gt;장점은 producer가 메시지 발송을 보장한다.&lt;/li&gt;
&lt;li&gt;단점으로는 consumer가 멱등성을 보장하도록 코딩해야 한다.&lt;/li&gt;
&lt;li&gt;발신 메시지 상태 관리 필요
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;consumer에서 잘 받았는지 확인하기 위함&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Exactly once&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;어려운 구현 난이도 &amp;amp; 메시지 큐에 의존적&lt;/li&gt;
&lt;li&gt;발신 &amp;amp; 수신 메시지 상태 관리 필요&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 비용대비 효율이 좋은 At least once를 보장하는 것을 선호하는 것 같다. 1번은 유실 가능성이 너무 크므로 신뢰성 있는 데이터를 보장하고자 한다면 피하는 것이 좋을 것 같고 3번은 최선이긴 하지만 비용이 너무 크다는 단점이 있기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 분산환경(MSA)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.49.16.png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byeukq/btsvkMj22uW/wAzEuIGTpmPJ8eDAliRw00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byeukq/btsvkMj22uW/wAzEuIGTpmPJ8eDAliRw00/img.png&quot; data-alt=&quot;서비스별 분산 환경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byeukq/btsvkMj22uW/wAzEuIGTpmPJ8eDAliRw00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyeukq%2FbtsvkMj22uW%2FwAzEuIGTpmPJ8eDAliRw00%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;687&quot; height=&quot;377&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.49.16.png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서비스별 분산 환경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 3가지 방법이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;RDB를 사용한 방법 (api)&lt;/li&gt;
&lt;li&gt;rabbit mq를 사용한 방법 (queue)&lt;/li&gt;
&lt;li&gt;kafka를 사용한 방법 (queue)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RDB를 사용한 방법 (Transactional Outbox &amp;amp; Polling Pattern)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 @TransactionalEventListener을 통해서 DB 커밋 후, rest api로 발송할 수 있다. 이러면 db에 save 되는 것을 확신하고 api를 쏘기 때문에 걱정이 없을 수 있다. 하지만 아래처럼 api에서 실패가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.52.21.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA1ZpI/btsvlHbKp0T/YKJkpIBS7cxYCRhJmUPz00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA1ZpI/btsvlHbKp0T/YKJkpIBS7cxYCRhJmUPz00/img.png&quot; data-alt=&quot;문제 상황 1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA1ZpI/btsvlHbKp0T/YKJkpIBS7cxYCRhJmUPz00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA1ZpI%2FbtsvlHbKp0T%2FYKJkpIBS7cxYCRhJmUPz00%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;596&quot; height=&quot;320&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.52.21.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;문제 상황 1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 DB에는 저장되지만 api발송에는 실패하게 되므로 데이터 정합성에 문제가 발생한다. 그러면 어떻게 처리해야 할까?&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;단순하게 아래의 사진처럼 @Retry(3번 시도하고, 간격마다 100ms 시간을 delay 함)로 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.53.28.png&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1XePU/btsvqzYf75I/ADTq5CxzDSnia8QgO06RUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1XePU/btsvqzYf75I/ADTq5CxzDSnia8QgO06RUK/img.png&quot; data-alt=&quot;retry&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1XePU/btsvqzYf75I/ADTq5CxzDSnia8QgO06RUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1XePU%2FbtsvqzYf75I%2FADTq5CxzDSnia8QgO06RUK%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;177&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.53.28.png&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;retry&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이러한 방법은 말 그대로 여러 번 시도하는 것이지 근본적인 해결은 되지 못한다. 예를 들면 외부 시스템의 장애로 인해 재시도한 것이 무의미할 수도 있기 때문이다. &lt;span style=&quot;color: #4c4c4c; text-align: start;&quot;&gt;또한 외부 시스템의 문제가 내부 시스템의 지연 및 장애로 전파될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.54.38.png&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cf0KN/btsvl9Z7yG3/FuaV4IrKKcRAQ4tekjOgF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cf0KN/btsvl9Z7yG3/FuaV4IrKKcRAQ4tekjOgF0/img.png&quot; data-alt=&quot;문제 상황2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cf0KN/btsvl9Z7yG3/FuaV4IrKKcRAQ4tekjOgF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCf0KN%2Fbtsvl9Z7yG3%2FFuaV4IrKKcRAQ4tekjOgF0%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;634&quot; height=&quot;371&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.54.38.png&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;문제 상황2&lt;/figcaption&gt;
&lt;/figure&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;Transactional Outbox Pattern과 Polling Publisher Pattern을 함께 사용하면 해결할 수 있다고 한다. 해당 방식은 고전적인 2PC를 사용하지 않는 방식이다. 간단하게 DB의 데이터를 주기적으로 읽고 publish 하는 패턴이라고 볼 수 있다.&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;Transactional Outbox Pattern&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.01.05.png&quot; data-origin-width=&quot;2298&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lcgjZ/btsvb5LV2al/iD5oZxlyj7XGIMv24XwrUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lcgjZ/btsvb5LV2al/iD5oZxlyj7XGIMv24XwrUK/img.png&quot; data-alt=&quot;transactional outbox pattern&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lcgjZ/btsvb5LV2al/iD5oZxlyj7XGIMv24XwrUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlcgjZ%2Fbtsvb5LV2al%2FiD5oZxlyj7XGIMv24XwrUK%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;2298&quot; height=&quot;820&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.01.05.png&quot; data-origin-width=&quot;2298&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;transactional outbox pattern&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox는 메일 송신함과 같다. &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;애플리케이션은 데이터베이스의 outbox 테이블에 메시지 내용을 저장한다. 그리고 다른 애플리케이션이나 프로세스는 outbox 테이블에서 데이터를 읽고 해당 데이터를 사용하여 작업을 수행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.57.34.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb0Kmd/btsvncI3IPL/6cpilkdkkUAwSm4d5lIPJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb0Kmd/btsvncI3IPL/6cpilkdkkUAwSm4d5lIPJ0/img.png&quot; data-alt=&quot;코드 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb0Kmd/btsvncI3IPL/6cpilkdkkUAwSm4d5lIPJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb0Kmd%2FbtsvncI3IPL%2F6cpilkdkkUAwSm4d5lIPJ0%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;685&quot; height=&quot;266&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.57.34.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;코드 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 outbox디비의 역할을 eventRepository가 한다고 생각하면 된다. 여기서 @Transactional로 묶여있으므로 task가 만약 실패하더라도 outbox에 event가 저장되지 않고 롤백되므로 원자성을 보장한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Polling Publisher Pattern&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.56.23.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAWOrs/btsvb4TNVjy/nfknkD3A5biJm42T1ffeL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAWOrs/btsvb4TNVjy/nfknkD3A5biJm42T1ffeL0/img.png&quot; data-alt=&quot;polling&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAWOrs/btsvb4TNVjy/nfknkD3A5biJm42T1ffeL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAWOrs%2Fbtsvb4TNVjy%2FnfknkD3A5biJm42T1ffeL0%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;563&quot; height=&quot;388&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.56.23.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;polling&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.59.36.png&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLZdun/btsvl7VxE2o/BhtL7biKjN670D3EJGrv6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLZdun/btsvl7VxE2o/BhtL7biKjN670D3EJGrv6K/img.png&quot; data-alt=&quot;polling&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLZdun/btsvl7VxE2o/BhtL7biKjN670D3EJGrv6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLZdun%2Fbtsvl7VxE2o%2FBhtL7biKjN670D3EJGrv6K%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;680&quot; height=&quot;322&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.59.36.png&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;polling&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;매 분 0초부터 5초 간격으로 작업&lt;/span&gt; &lt;/span&gt;Event 상태가 READY인 객체를 찾아서 restTemplate으로 이벤트를 발송하고 event를 발송했다는 상태로 표시해 주는 로직이다.&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;아래는 DB 테이블을 설계할 때, 넣으면 좋은 컬럼들을 추천해 주신 것이라고 한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.56.59.png&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chK7SQ/btsvdnZH67j/5jTA6TFXc4UWl7jDLYoPy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chK7SQ/btsvdnZH67j/5jTA6TFXc4UWl7jDLYoPy0/img.png&quot; data-alt=&quot;outbox column&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chK7SQ/btsvdnZH67j/5jTA6TFXc4UWl7jDLYoPy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchK7SQ%2FbtsvdnZH67j%2F5jTA6TFXc4UWl7jDLYoPy0%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;1356&quot; height=&quot;530&quot; data-filename=&quot;스크린샷 2023-09-21 오후 7.56.59.png&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;outbox column&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;다시 본문으로 돌아와서,&lt;span&gt; &lt;/span&gt;&lt;/span&gt;이러한 Transactional Outbox Pattern과 Polling Publisher Pattern을 통해 At least once를 보장함으로써 restAPI에서 데이터 유실 문제를 해결할 수 있다.&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;polling후 publisher 과정에 의한 지연 처리&lt;/li&gt;
&lt;li&gt;데이터베이스 부하 &amp;amp; 디비 비례한 처리속도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 At least once에서 항상 멱등한 처리를 해줘야하기에 아래와 같은 멱등 방법도 고려해줘야 한다.&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;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1-1. 트랜잭션 로그 테일링 패턴(Transaction log tailing Pattern)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.31.47.png&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coCQT0/btsvdt6IWBW/x0uj6vTwRihdycKGjhznpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coCQT0/btsvdt6IWBW/x0uj6vTwRihdycKGjhznpK/img.png&quot; data-alt=&quot;로그 테일링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coCQT0/btsvdt6IWBW/x0uj6vTwRihdycKGjhznpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoCQT0%2Fbtsvdt6IWBW%2Fx0uj6vTwRihdycKGjhznpK%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;503&quot; height=&quot;387&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.31.47.png&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그 테일링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;해당 방식은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;DB 트랜잭션 로그(커밋 로그)를 테일링(tailing)하는 방법이다. 애플리케이션에서 커밋된 업데이트는 각 DB의 트랜잭션 로그 항목(log entry, 로그 엔트리)으로 남는다. 트랜잭션 로그 마이너(transaction log miner)로 트랜잭션 로그를 읽어 변경분을 하나씩 메시지로 메시지 브로커에 발행하는 방법이다. (aws cloud trailing처럼 모든 활동을 읽는다고 생각했습니다.)&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;RDBMS의 outbox 테이블에 출력된 메시지 또는 NoSQL DB에 레코드에 추가된 메시지를 발행할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;mysql의 경우 mysqlbinlog, PostgreSQL WAL, Oracle redolog 등을 활용하여 변경사항을 읽어서 구현할 수 있지만 구현 난이도가 높아 관련 툴을 사용하는 경우가 많다고 한다. 관련 툴은 디비지움(Debezium), 링크드인 데이터 버스(LinkdIn Databus), DynamoDB 스트림즈, 이벤추에이트 트램 등이 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;장점으로는 2PC를 사용하지 않고 따로 메시지를 위한 로직도 필요하지 않아서 장점만 있어 보였다. 하지만 단점은 아래와 같다.&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;데이터베이스별 솔루션이 필요하다.(추상화 X)&lt;/li&gt;
&lt;li&gt;멱등성을 보장해야 함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 로그를 읽고 처리하는 것을 &lt;b&gt;CDC(Change Data Capture)&lt;/b&gt;라고 한다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Rabbit MQ를 사용한 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ack를 메시지 처리 응답 메커니즘&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;producer confirm - producer에서 메시지 잘 넣었을 때&lt;/li&gt;
&lt;li&gt;consumer Ack - consumer가 응답 잘 보냈을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.19.07.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rmVsQ/btsvqzcVkjI/jOoTMVZn6XdmOIYUqcfYd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rmVsQ/btsvqzcVkjI/jOoTMVZn6XdmOIYUqcfYd0/img.png&quot; data-alt=&quot;큐 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rmVsQ/btsvqzcVkjI/jOoTMVZn6XdmOIYUqcfYd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrmVsQ%2FbtsvqzcVkjI%2FjOoTMVZn6XdmOIYUqcfYd0%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;1230&quot; height=&quot;322&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.19.07.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;큐 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Producer Confirm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 라우팅에 실패한다면? exchange가 Ack or NACK를 응답함으로써 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.39.31.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q407z/btsvb4sEXb6/cJTLC9fZDuuwTfbT8ALWek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q407z/btsvb4sEXb6/cJTLC9fZDuuwTfbT8ALWek/img.png&quot; data-alt=&quot;producer confirm&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q407z/btsvb4sEXb6/cJTLC9fZDuuwTfbT8ALWek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ407z%2Fbtsvb4sEXb6%2FcJTLC9fZDuuwTfbT8ALWek%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;1326&quot; height=&quot;464&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.39.31.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;producer confirm&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer Confirm을 어떻게 확인하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer Confirm을 확인하는 방법은 &lt;b&gt;CorrelationData.java&lt;/b&gt;로 확인할 수 있다. rabbitTemplate을 사용하여 메시지를 발행할 때, CorrelationData를 같이 넘겨서 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.23.53.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpxXsb/btsviTYmUmL/PCNg72fEHOpS0gyKV1WHx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpxXsb/btsviTYmUmL/PCNg72fEHOpS0gyKV1WHx0/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpxXsb/btsviTYmUmL/PCNg72fEHOpS0gyKV1WHx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpxXsb%2FbtsviTYmUmL%2FPCNg72fEHOpS0gyKV1WHx0%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;1776&quot; height=&quot;522&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.23.53.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 발행 시점에, CorrelationData에 UUID를 넣으면 메시지 확인 가능하다. 콜백을 받는 방법은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.25.08.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bscNJu/btsvjgTCytb/ypKOJLJKgDx6YGuFxUKtOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bscNJu/btsvjgTCytb/ypKOJLJKgDx6YGuFxUKtOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bscNJu/btsvjgTCytb/ypKOJLJKgDx6YGuFxUKtOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbscNJu%2FbtsvjgTCytb%2FypKOJLJKgDx6YGuFxUKtOk%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;1504&quot; height=&quot;958&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.25.08.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ack = boolean으로 성공, 실패 확인&lt;/li&gt;
&lt;li&gt;cause = 실패일 때 원인을 알 수 있다.&lt;/li&gt;
&lt;li&gt;콜백 메서드라 실시간 대응은 어렵지만, 어떤 메시지가 실패했는지 알 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정도 따로 해줘야 한다. 스프링부트를 사용한다면 아래처럼 설정한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스프링부트&lt;/li&gt;
&lt;li&gt;바닐라 스프링 사용할 때&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.26.17.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5HRs8/btsviR7g8Ji/n0bsqKyQtxDKHYxaVx2GFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5HRs8/btsviR7g8Ji/n0bsqKyQtxDKHYxaVx2GFK/img.png&quot; data-alt=&quot;설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5HRs8/btsviR7g8Ji/n0bsqKyQtxDKHYxaVx2GFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5HRs8%2FbtsviR7g8Ji%2Fn0bsqKyQtxDKHYxaVx2GFK%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;1514&quot; height=&quot;702&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.26.17.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Consumer Acknowledge&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.47.32.png&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xcLYX/btsviV2VIBU/7BXdVRlUgghFXsRH59i7Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xcLYX/btsviV2VIBU/7BXdVRlUgghFXsRH59i7Kk/img.png&quot; data-alt=&quot;Consumer Acknowledge&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xcLYX/btsviV2VIBU/7BXdVRlUgghFXsRH59i7Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxcLYX%2FbtsviV2VIBU%2F7BXdVRlUgghFXsRH59i7Kk%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;1328&quot; height=&quot;450&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.47.32.png&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Consumer Acknowledge&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: center;&quot;&gt;Consumer Acknowledge&lt;/span&gt;을 어떻게 확인하는가?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: center;&quot;&gt;Consumer Acknowledge&lt;/span&gt;을 확인하는 방법은&lt;b&gt;Channel.java&lt;/b&gt;로 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최신 RabbitMQ버전부터는 자동으로 ack를 지원을 해준다. 따라서 아래와 같은 방식을 적용하면 ack를 두 번 보내서 - unknown delivery tag 1, 이와 같은 에러를 계속 마주할 것이다..&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.49.04.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO5VOd/btsvdteCTvf/vY7Hxk6JPvLbAnmTfcFhLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO5VOd/btsvdteCTvf/vY7Hxk6JPvLbAnmTfcFhLk/img.png&quot; data-alt=&quot;@RabbitListener 사용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO5VOd/btsvdteCTvf/vY7Hxk6JPvLbAnmTfcFhLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO5VOd%2FbtsvdteCTvf%2FvY7Hxk6JPvLbAnmTfcFhLk%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;1666&quot; height=&quot;608&quot; data-filename=&quot;스크린샷 2023-09-23 오후 4.49.04.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@RabbitListener 사용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1695491283443&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # 수동으로 동작하도록 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto를 사용하지 않고 수동으로 적용하는 이유?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Auto인 경우는 queue에서 메시지를 받자마자 ack를 보낸다. ack를 받은 큐는 데이터를 바로 삭제한다. 하지만 우리가 메시지를 처리하는 시간에 예외가 발생할때, Auto이면 이미 ack를 보내서 데이터가 유실이 된다. 따라서 메시지로부터 받은 데이터가 정상 처리 되었는지 확인하고 Ack를 보내도록 manual한 설정을 가져가서 수동으로 보내는 것이다. 이외에도 아래 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;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; NACK&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;/li&gt;
&lt;li&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;&amp;nbsp;바로 ACK&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; ACK &lt;/span&gt;또는&lt;span&gt; NACK&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;/li&gt;
&lt;li&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;&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;span&gt;, ACK&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; ACK&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;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.31.19.png&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPs22x/btsvkSYSrZ1/VQlJVQZEoS97t4eufxdBKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPs22x/btsvkSYSrZ1/VQlJVQZEoS97t4eufxdBKK/img.png&quot; data-alt=&quot;계속해서 NACK가 온 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPs22x/btsvkSYSrZ1/VQlJVQZEoS97t4eufxdBKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPs22x%2FbtsvkSYSrZ1%2FVQlJVQZEoS97t4eufxdBKK%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;1274&quot; height=&quot;418&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.31.19.png&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;계속해서 NACK가 온 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;계속해서 NACK응답이 오면 큐가 쌓일 것이다. 이럴 땐, Dead Letter Queue로 넘겨서 처리할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;reQueue가 fulls고 basic.NACK 나 basic.reject로 처리하는 경우&lt;/li&gt;
&lt;li&gt;Queue에 메시지가 ttl을 넘어서 오래 있는 경우&lt;/li&gt;
&lt;li&gt;Queue가 가득 찼을 때&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.31.49.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T4Wav/btsvkMK7XVz/x1kTdym6DAiXdAsypARH1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T4Wav/btsvkMK7XVz/x1kTdym6DAiXdAsypARH1K/img.png&quot; data-alt=&quot;Dead Letter Queue&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T4Wav/btsvkMK7XVz/x1kTdym6DAiXdAsypARH1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT4Wav%2FbtsvkMK7XVz%2Fx1kTdym6DAiXdAsypARH1K%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;1308&quot; height=&quot;582&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.31.49.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Dead Letter Queue&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Retry 후 DeadLetter로 이동&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.34.32.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF6o6Z/btsvoXZf1t3/IY9j9r9RppOVfk3mRAExk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF6o6Z/btsvoXZf1t3/IY9j9r9RppOVfk3mRAExk1/img.png&quot; data-alt=&quot;예시 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF6o6Z/btsvoXZf1t3/IY9j9r9RppOVfk3mRAExk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF6o6Z%2FbtsvoXZf1t3%2FIY9j9r9RppOVfk3mRAExk1%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;1834&quot; height=&quot;648&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.34.32.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;최대 3번 실행 후&lt;/li&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;retry간격으로 1000ms, 2의 배수로 최대 간격은 2000ms&lt;/li&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;원래 있던 requeue에 계속하지 말고 DeadLetter로 빠지도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.36.17.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MSUvj/btsviSLUEn0/U8OoAhO4excDeCtF8Fbupk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MSUvj/btsviSLUEn0/U8OoAhO4excDeCtF8Fbupk/img.png&quot; data-alt=&quot;RabbitListener로 만든 deadLetter&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MSUvj/btsviSLUEn0/U8OoAhO4excDeCtF8Fbupk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMSUvj%2FbtsviSLUEn0%2FU8OoAhO4excDeCtF8Fbupk%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;1212&quot; height=&quot;524&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.36.17.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RabbitListener로 만든 deadLetter&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Kafka를 사용한 방법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1. Producer Confirm&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.51.26.png&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGJviN/btsvdqvnG8x/O5qI7wSJRkqDkFSba6Syik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGJviN/btsvdqvnG8x/O5qI7wSJRkqDkFSba6Syik/img.png&quot; data-alt=&quot;예시 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGJviN/btsvdqvnG8x/O5qI7wSJRkqDkFSba6Syik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGJviN%2FbtsvdqvnG8x%2FO5qI7wSJRkqDkFSba6Syik%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;1646&quot; height=&quot;818&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.51.26.png&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ListenableFuture에 2개의 콜백 가능, 첫 번째는 성공 콜백, 두 번째는 실패했을 때 콜백이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2. Consumer Acknowledge&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.53.44.png&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cy6Ft/btsvkNDksDB/yZZSHbiJPq1gPgrfMRaLI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cy6Ft/btsvkNDksDB/yZZSHbiJPq1gPgrfMRaLI0/img.png&quot; data-alt=&quot;예시 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cy6Ft/btsvkNDksDB/yZZSHbiJPq1gPgrfMRaLI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCy6Ft%2FbtsvkNDksDB%2FyZZSHbiJPq1gPgrfMRaLI0%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;1658&quot; height=&quot;604&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.53.44.png&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.55.59.png&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRJwGT/btsvdszZy8J/dj3CbsIL4W2hD5okFcG29k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRJwGT/btsvdszZy8J/dj3CbsIL4W2hD5okFcG29k/img.png&quot; data-alt=&quot;onMessage 예시 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRJwGT/btsvdszZy8J/dj3CbsIL4W2hD5okFcG29k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRJwGT%2FbtsvdszZy8J%2Fdj3CbsIL4W2hD5okFcG29k%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;1826&quot; height=&quot;816&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.55.59.png&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;onMessage 예시 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번째 인자로 받은 acknowledgment.acknowledge()를 호출하면 성공한 Consumer ACK이 나간다. 아래의 설정값도 반드시 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.56.34.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcbCGv/btsvqAv7Ro5/907rsiBHDjTykFq6p7ftk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcbCGv/btsvqAv7Ro5/907rsiBHDjTykFq6p7ftk1/img.png&quot; data-alt=&quot;설정 필수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcbCGv/btsvqAv7Ro5/907rsiBHDjTykFq6p7ftk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcbCGv%2FbtsvqAv7Ro5%2F907rsiBHDjTykFq6p7ftk1%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;1670&quot; height=&quot;716&quot; data-filename=&quot;스크린샷 2023-09-21 오후 11.56.34.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;716&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설정 필수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템 환경에서 메시지는 땔 수 없는 선택지인 것 같다. 성능에 따라 최대 한번 or 최소 한번으로 설정하면서 분산 환경에서 효율적으로 데이터를 송수신하도록 해야겠다.&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;*Scheduled cron&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 아래는 scheduled의 cron 표현식을 사용하여 스케줄링을 정의하는 방법입니다. 이 표현식은 초, 분, 시간, 일, 월, 요일 순서로 구성되며, 각각에 해당하는 위치에 어떤 동작을 수행할지를 지정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #f7f7f8; color: #374151; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초 (0-59)&lt;/li&gt;
&lt;li&gt;분 (0-59)&lt;/li&gt;
&lt;li&gt;시간 (0-23)&lt;/li&gt;
&lt;li&gt;일 (1-31)&lt;/li&gt;
&lt;li&gt;월 (1-12 또는 JAN-DEC)&lt;/li&gt;
&lt;li&gt;요일 (0-7 또는 SUN-SAT, 0과 7은 일요일)&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #f7f7f8; color: #374151; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;*/5 * * * * *: 5초 간격으로 실행&lt;/li&gt;
&lt;li&gt;0 0 * * * *: 매 시간 정각에 실행&lt;/li&gt;
&lt;li&gt;0 0 12 * * ?: 매일 정오(12시)에 실행&lt;/li&gt;
&lt;li&gt;0 15 10 ? * *: 매일 10:15에 실행&lt;/li&gt;
&lt;li&gt;0 15 10 * * ?: 매일 10:15에 실행&lt;/li&gt;
&lt;li&gt;0 15 10 * * ? 2019: 2019년에 매일 10:15에 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;cron 이외에 @Scheduled(fixedRate=1000) // 1초마다 실행이라는 설정도 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://microservices.io/patterns/data/transactional-outbox.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695451779034&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;Microservices Pattern: Transactional outbox&quot; data-og-description=&quot;First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.&quot; data-og-host=&quot;microservices.io&quot; data-og-source-url=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; data-og-url=&quot;http://microservices.io/patterns/data/transactional-outbox.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bSeOx9/hyT2xaggzX/ruQN8qAbtkiGiaX6Nl1DRK/img.png?width=1298&amp;amp;height=461&amp;amp;face=0_0_1298_461,https://scrap.kakaocdn.net/dn/ua7ZA/hyTY7qMoWi/6KRZLW7iaIWZgT4wtCsLKk/img.jpg?width=720&amp;amp;height=903&amp;amp;face=0_0_720_903,https://scrap.kakaocdn.net/dn/efw5Xy/hyT2EHd9Kr/Oi4G60Sfo4B3c2gqLCGZp1/img.png?width=1377&amp;amp;height=445&amp;amp;face=0_0_1377_445&quot;&gt;&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bSeOx9/hyT2xaggzX/ruQN8qAbtkiGiaX6Nl1DRK/img.png?width=1298&amp;amp;height=461&amp;amp;face=0_0_1298_461,https://scrap.kakaocdn.net/dn/ua7ZA/hyTY7qMoWi/6KRZLW7iaIWZgT4wtCsLKk/img.jpg?width=720&amp;amp;height=903&amp;amp;face=0_0_720_903,https://scrap.kakaocdn.net/dn/efw5Xy/hyT2EHd9Kr/Oi4G60Sfo4B3c2gqLCGZp1/img.png?width=1377&amp;amp;height=445&amp;amp;face=0_0_1377_445');&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;Microservices Pattern: Transactional outbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;microservices.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.gangnamunni.com/post/transactional-outbox/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://blog.gangnamunni.com/post/transactional-outbox/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695456092075&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;Transactional Outbox Pattern을 이용한 결과적 일관성 확보 by 강남언니 블로그&quot; data-og-host=&quot;blog.gangnamunni.com&quot; data-og-source-url=&quot;https://blog.gangnamunni.com/post/transactional-outbox/&quot; data-og-url=&quot;https://blog.gangnamunni.com/post/transactional-outbox/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cEXLhZ/hyTY0kRXWi/AHoY4CEgG8KK4UXBkWJ9HK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bFwhKp/hyT2uxQS8u/KmkUM0yQMuLcWTY65QHgDK/img.jpg?width=1602&amp;amp;height=1522&amp;amp;face=0_0_1602_1522,https://scrap.kakaocdn.net/dn/cdERGk/hyT2vQ31z6/7uhDzZ4TeUJi3CVYwrZkok/img.jpg?width=1502&amp;amp;height=882&amp;amp;face=0_0_1502_882&quot;&gt;&lt;a href=&quot;https://blog.gangnamunni.com/post/transactional-outbox/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.gangnamunni.com/post/transactional-outbox/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cEXLhZ/hyTY0kRXWi/AHoY4CEgG8KK4UXBkWJ9HK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bFwhKp/hyT2uxQS8u/KmkUM0yQMuLcWTY65QHgDK/img.jpg?width=1602&amp;amp;height=1522&amp;amp;face=0_0_1602_1522,https://scrap.kakaocdn.net/dn/cdERGk/hyT2vQ31z6/7uhDzZ4TeUJi3CVYwrZkok/img.jpg?width=1502&amp;amp;height=882&amp;amp;face=0_0_1502_882');&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;Transactional Outbox Pattern을 이용한 결과적 일관성 확보 by 강남언니 블로그&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.gangnamunni.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/uk5fRLUsBfk?si=Pz2QeBCben4BQcEH&quot;&gt;https://youtu.be/uk5fRLUsBfk?si=Pz2QeBCben4BQcEH&lt;/a&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;a href=&quot;https://www.youtube.com/watch?v=VIbMOSciFhg&quot;&gt;https://www.youtube.com/watch?v=VIbMOSciFhg&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=VIbMOSciFhg&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/rBvWp/hyT2t6RarU/EirwdyITAu9BKiNuYzL5c0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/VIbMOSciFhg&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/63953773/rabbitmq-does-basic-ack-also-needs-to-be-confirmed-by-the-server&quot;&gt;https://stackoverflow.com/questions/63953773/rabbitmq-does-basic-ack-also-needs-to-be-confirmed-by-the-server&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>멱등성</category>
      <category>분산 시스템</category>
      <category>스케줄링</category>
      <category>이벤트</category>
      <category>큐</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/156</guid>
      <comments>https://giron.tistory.com/156#entry156comment</comments>
      <pubDate>Sat, 23 Sep 2023 17:43:11 +0900</pubDate>
    </item>
    <item>
      <title>[Tomcat]NIO Connector를 중심으로</title>
      <link>https://giron.tistory.com/155</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣은 was로서 내부에 웹 서버와 웹 컨테이너(서블릿 컨테이너)로 이루어져 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-15 오후 3.07.13.png&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YFEhL/btr35xeX1Md/dd6gUnXtSXpbqJW25kb7v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YFEhL/btr35xeX1Md/dd6gUnXtSXpbqJW25kb7v0/img.png&quot; data-alt=&quot;was 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YFEhL/btr35xeX1Md/dd6gUnXtSXpbqJW25kb7v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYFEhL%2Fbtr35xeX1Md%2Fdd6gUnXtSXpbqJW25kb7v0%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;1428&quot; height=&quot;536&quot; data-filename=&quot;스크린샷 2023-03-15 오후 3.07.13.png&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;was 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 정적인 페이지만 줬었기 때문에 웹서버만 있으면 됐다. 하지만 동적인 페이지를 요구하기 시작했고 CGI(Common gateway Interface)가 나왔다. 하지만 CGI는 요청마다 프로세스를 생성해서 처리해 줬고 요청이 많아지니 메모리 용량에 한계가 있다. 따라서 자바에서는 서블릿을 통해서 해결했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서블릿은 프로세스가 아닌 스레드를 생성해서 처리한다. 또한 자바로 이루어져 있어서 GC로 인해 메모리 누수를 걱정하지 않아도 되었다. 이런 서블릿을 관리하는 게 서블릿 컨테이너고 톰캣의 was가 이 서블릿 컨테이너로 이루어져 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서블릿은 init &amp;rarr; service &amp;rarr; destroy의 생명주기를 갖는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;톰캣 내부에서 카탈리나는 톰캣 엔진으로 요청에 대한 커넥터들을 처리하는 파이프라인이다. 예를 들어, 80 포트로 코요테가 요청을 받으면 카탈리나에게 연결해서 카탈리나가 요청 응답을 처리하고 결과값을 반환해 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-15 오후 3.11.04.png&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dx1wMm/btr3V7WuZZs/Ki40fs8LbmqyS7ieZT34x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dx1wMm/btr3V7WuZZs/Ki40fs8LbmqyS7ieZT34x1/img.png&quot; data-alt=&quot;Tomcat Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dx1wMm/btr3V7WuZZs/Ki40fs8LbmqyS7ieZT34x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdx1wMm%2Fbtr3V7WuZZs%2FKi40fs8LbmqyS7ieZT34x1%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;1404&quot; height=&quot;964&quot; data-filename=&quot;스크린샷 2023-03-15 오후 3.11.04.png&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Tomcat Architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹서버와 was 분리를 추천하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;was혼자서만 처리하면 부하가 크다. 웹서버에서 정적 자원을 뱉어주고 was에서 동적인 자원들을 주도록 분리한다. 보안적으로도 was는 비즈니스로직이 존재한다. 따라서 내부망을 사용하고 웹서버를 외부망으로 두어 분리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat에서는 일반적으로 HTTP 요청에 대한 처리를 위해 네트워크 I/O 방식으로 BIO Connector(Blocking I/O) 또는 NIO Connector(Non-blocking I/O) 방식을 선택할 수 있다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Connector&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소켓을 연결하고 데이터 패킷을 얻어 ServletRequest 객체를 생성하여 Servlet Container에게 전달해 주는 역할.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선, port listen을 통해 Socket Connection을 얻게 됩니다.&lt;/li&gt;
&lt;li&gt;Socket Connection으로부터 데이터 패킷을 획득.&lt;/li&gt;
&lt;li&gt;데이터 패킷을 파싱 해서 ServletRequest Object를 생성합니다.&lt;/li&gt;
&lt;li&gt;얻어진 ServletRequest Object를 알맞은 Servlet Container에게 보냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BIO Connector&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BIO(Blocking I/O)는 입출력 작업을 수행할 때,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;thread pool에서 한 thread가 반환되어 소켓 연결을 받고&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;요청을 처리하고 요청에 대해 응답한 후 &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;소켓 연결이 종료되면 &lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;thread&lt;/span&gt; pool에 다시 돌아온다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thread는 &lt;b&gt;해당 작업이 완료될 때까지 대기&lt;/b&gt;하며, 그동안 다른 작업을 처리하지 않는 방식이다. 따라서 한 스레드가 하나의 클라이언트의 요청만 처리할 수 있고, 다수의 클라이언트가 요청을 보내면, 대기열에 대기하다가 하나씩 순차적으로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 단순하지만, 다수의 클라이언트 요청을 동시에 처리할 수 없기 때문에, 성능이 저하될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NIO Connector&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;nio connector는 내부적으로 Java NIO를 사용한다. 따라서 관련 키워드를 맛보고 간다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java NIO&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;J&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;ava 1.4 버전 이후 등장한 NIO(New Input Output)를 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Channel&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;J&lt;span style=&quot;text-align: left;&quot;&gt;ava io의 stream의 대체라고 볼 수 있다. 파일에서 읽고 쓰는 역할로 File Channel, Socket Channel 등 다양하게 존재한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;stream은 양방향이 안 돼서 입력, 출력 stream을 만들어야 했다. 하지만 channel은 양방향이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동기, 비동기 모두 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;항상 Buffer와 함께 사용된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Buffer&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;서버에서 클라이언트와 데이터를 주고받을 때 채널을 통해서 버퍼(ByteBuffer)를 이용해 읽고 쓴다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;java의 기존의 io는  buffer가 없어서 byte마다 처리해서 디스크나 네트워크 접근 오버헤드로 성능이 좋지 않았다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Selector&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java NIO에는 여러 개의 채널에서 이벤트(예: 연결 생성, 데이터 도착 등)를 모니터링할 수 있는 설렉터가 포함돼 있기 때문에 하나의 스레드로 여러 채널을 모니터링할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;논블로킹(non-blocking) I/O&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java NIO에서는 논블로킹 I/O를 사용할 수&lt;u&gt;도&lt;/u&gt; 있다. 예를 들어, 스레드가 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;channel 한테 &lt;/span&gt;버퍼에서 데이터를 읽어달라고 요청하면, 채널이 버퍼에 데이터를 채워 넣는 동안 해당 스레드는 다른 작업을 수행할 수 있다. 이후 채널이 버퍼에 데이터를 채워 넣고 나면 스레드는 해당 버퍼를 이용해 계속 처리를 진행할 수 있다. 반대로 데이터를 채널로 보내는 경우에도 논블로킹으로 처리할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Selector가 지원하는 메서드&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;select(): 등록된 채널 중에서 I/O 이벤트가 발생한 채널의 수를 반환&lt;/li&gt;
&lt;li&gt;select(long timeout): timeout 시간 동안 등록된 채널 중에서 I/O 이벤트가 발생한 채널의 수를 반환&lt;/li&gt;
&lt;li&gt;selectedKeys(): 이벤트가 발생한 채널의 Set을 반환&lt;/li&gt;
&lt;li&gt;selectNow(): 등록된 채널 중에서 I/O 이벤트가 발생한 채널의 수를 반환. 이 메서드는 select()와 달리 블로킹되지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Tomcat에서 NIO Connector 동작 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Tomcat 8.X부터 기본으로 NIO로 동작한다.&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;NIO Connector에서는 BIO와 달리, 연결이 발생할 때 바로 새로운 Thread를 할당하지 않고(Connection : Thread &amp;ne; 1:1) Poller라는 개념의 Thread에게 Connection(Channel)을 넘겨준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Poller는 Socket들을 캐시로 들고 있다가 해당 Socket에서 data에 대한 처리가 가능한 순간에만 thread를 할당하는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-16 오후 4.53.10.png&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P7ySa/btr4hyymfWW/wTgQStSTpLkYOKZNDkdQ2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P7ySa/btr4hyymfWW/wTgQStSTpLkYOKZNDkdQ2K/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P7ySa/btr4hyymfWW/wTgQStSTpLkYOKZNDkdQ2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP7ySa%2Fbtr4hyymfWW%2FwTgQStSTpLkYOKZNDkdQ2K%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;2392&quot; height=&quot;918&quot; data-filename=&quot;스크린샷 2023-03-16 오후 4.53.10.png&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;Acceptor는 Socket Connection을 accept 한다. 소켓에서 Socket Channel 객체를 얻어서 NioSocketWrapper -&amp;gt; PollerEvent&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;객체로 변환한다. &lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;그리고 이 객체를 PollerEvent Queue에 넣게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-16 오후 5.09.26.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rGeEP/btr4blfL0OP/1lnPAAfodTHGixxgSrL2zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rGeEP/btr4blfL0OP/1lnPAAfodTHGixxgSrL2zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rGeEP/btr4blfL0OP/1lnPAAfodTHGixxgSrL2zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrGeEP%2Fbtr4blfL0OP%2F1lnPAAfodTHGixxgSrL2zK%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;559&quot; height=&quot;400&quot; data-filename=&quot;스크린샷 2023-03-16 오후 5.09.26.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;Acceptor는 event Queue의 producer, Poller thread는 event Queue의 consumer입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dn23S9/btr36F7dQcv/wFlQJXSUKGcKSXsqkItsk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dn23S9/btr36F7dQcv/wFlQJXSUKGcKSXsqkItsk1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;1018&quot; data-filename=&quot;스크린샷 2023-03-16 오후 4.31.30.png&quot; width=&quot;316&quot; height=&quot;286&quot; style=&quot;width: 40.2199%; margin-right: 10px;&quot; data-widthpercent=&quot;40.69&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dn23S9/btr36F7dQcv/wFlQJXSUKGcKSXsqkItsk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdn23S9%2Fbtr36F7dQcv%2FwFlQJXSUKGcKSXsqkItsk1%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;1126&quot; height=&quot;1018&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8VXey/btr4ihC8N7n/dFKypyUWIcvyx0CiqAwhzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8VXey/btr4ihC8N7n/dFKypyUWIcvyx0CiqAwhzK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;598&quot; data-filename=&quot;스크린샷 2023-03-16 오후 5.14.14.png&quot; width=&quot;483&quot; height=&quot;300&quot; data-widthpercent=&quot;59.31&quot; style=&quot;width: 58.6173%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8VXey/btr4ihC8N7n/dFKypyUWIcvyx0CiqAwhzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8VXey%2Fbtr4ihC8N7n%2FdFKypyUWIcvyx0CiqAwhzK%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;964&quot; height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Poller(selector)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하나의 Poller 스레드 속 Selector를 사용하여 &lt;span style=&quot;text-align: start;&quot;&gt;&lt;b&gt;하나의 스레드로 여러 채널을  처리&lt;/b&gt;한다. Selector에 PollerEvent에서 받은 Channel을 등록한다. select() 동작으로 데이터를 읽을 수 있는 소켓을 얻고, &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Worker Thread Pool에서 이용할 수 있는 Woker Thread를 얻어서 해당 소켓을 worker thread에게 넘기게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;마무리로(이후부터 BIO와 동일)&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Worker Thread 내에서 소켓에서 얻은 &lt;b&gt;Http 요청을 처리하는 작업을 끝내고 HttpServletRequest Object로 변환 후&lt;/b&gt;,&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&amp;nbsp;알맞은 Servelt에게 Reqeust Object를 전달해서 servlet &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;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;즉, Selector를 사용해서 data 처리가 가능할 때만 Thread를 사용하기 때문에 idle 상태로 낭비되는 Thread가 줄어들게 된다. 또한 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;HTTP connectors(based on APR or NIO/NIO2)를 사용하면 비동기를 사용하므로 대량의 정적 파일을 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;send할때도 효율적으로 처리할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NIO Connector 동작 순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Acceptor가 소켓의 요청을 받는다.&lt;/li&gt;
&lt;li&gt;소켓에서 객체를 얻어 PollerEvent 객체로 변환해 준다.&lt;/li&gt;
&lt;li&gt;PollerEvent Queue에 넣는다.&lt;/li&gt;
&lt;li&gt;Poller thread 속 Selector Object를 이용하여 여러 채널을 관리한다.&lt;/li&gt;
&lt;li&gt;상태를 모니터링하다가 데이터를 읽을 수 있는 소켓을 얻고, worker thread를 얻으면 해당 소켓을 thread에 연결해 준다.&lt;/li&gt;
&lt;li&gt;worker thread에서 작업을 처리하면 해당 소켓으로 응답을 건네주면서 끝.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-16 오후 3.51.07.png&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w794t/btr4gUIdFGG/x84VEjN2G2bC1iDHSC0R8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w794t/btr4gUIdFGG/x84VEjN2G2bC1iDHSC0R8K/img.png&quot; data-alt=&quot;spring boot 2.7X 에서 내장 tomcat 9.0은 nio 이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w794t/btr4gUIdFGG/x84VEjN2G2bC1iDHSC0R8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw794t%2Fbtr4gUIdFGG%2Fx84VEjN2G2bC1iDHSC0R8K%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;216&quot; height=&quot;271&quot; data-filename=&quot;스크린샷 2023-03-16 오후 3.51.07.png&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;spring boot 2.7X 에서 내장 tomcat 9.0은 nio 이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;88f19326-756b-479d-8b66-fce7484c87b1&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tomcat.apache.org/tomcat-9.0-doc/aio.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tomcat.apache.org/tomcat-9.0-doc/aio.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/naverfinancial/servlet-tomcat-apache-nginx-%EC%9D%B4%EA%B2%8C-%EB%8B%A4-%EB%AD%98%EA%B9%8C-384cdeb9ad5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/naverfinancial/servlet-tomcat-apache-nginx-%EC%9D%B4%EA%B2%8C-%EB%8B%A4-%EB%AD%98%EA%B9%8C-384cdeb9ad5&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>Bio</category>
      <category>NIO</category>
      <category>Tomcat</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/155</guid>
      <comments>https://giron.tistory.com/155#entry155comment</comments>
      <pubDate>Sat, 18 Mar 2023 17:56:48 +0900</pubDate>
    </item>
    <item>
      <title>[Event] 이벤트를 이용한 성능 개선 및 의존성 분리 경험</title>
      <link>https://giron.tistory.com/154</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 한창 할 당시에는 몰랐었지만 추후에 학습하면서 Event처리를 통해 의존성을 끊는 방법을 알았다. 기존의 공식 프로젝트도 외부 api사용 로직을 분리하고 인터페이스를 통해서 의존성을 많이 줄였다고 생각했는데 여전히 불필요한 의존성이 엮여있었다.&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;h3 data-ke-size=&quot;size23&quot;&gt;문제 정의&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;임시 저장 게시글을 등록할 시, 임시 저장 게시글 삭제 로직&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는&amp;nbsp;&lt;u&gt;게시글 임시 저장&lt;/u&gt; -&amp;gt;&amp;nbsp;&lt;u&gt;임시 저장된 게시글을 등록&lt;/u&gt; -&amp;gt; &lt;u&gt;게시글은 저장되고 임시 저장 글은 제거&lt;/u&gt;하는 로직입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 로직에서 구체적으로 2가지 문제를 가집니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Article -&amp;gt; TempArticle의 의존성 생성
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시글을 저장하는 로직에서 임시 저장글이 어떻게 처리 되어야 하는지를 알 필요가 없다고 생각했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;하나의 트랜잭션으로 묶여서 동기적 처리 -&amp;gt; 즉, 성능 저하
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자는 게시글이 삭제되기만 기다리면 됩니다. 임시 저장글이 삭제되는 것은 1~2초 늦게 작동하더라도 큰 문제가 없다는 것입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ9178/btrWbo6oaTK/XTmzTNxau3qaU8rqW4i601/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ9178/btrWbo6oaTK/XTmzTNxau3qaU8rqW4i601/img.png&quot; data-alt=&quot;의존성 결합&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ9178/btrWbo6oaTK/XTmzTNxau3qaU8rqW4i601/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ9178%2FbtrWbo6oaTK%2FXTmzTNxau3qaU8rqW4i601%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;897&quot; height=&quot;288&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;의존성 결합&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이벤트를 통한 의존성 분리&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@Async&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EnableAsync&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 스프링의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@Async&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;어노테이션을 감지한다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;@Async는 스프링 AOP로 동작하는 self-invocation을 피해야하고 public으로 선언한 메서드에만 적용이 가능하다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;비동기적으로 메서드를 실행하기 위해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;SimpleAsyncTaskExecutor&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;를 사용한다. &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;(&lt;/span&gt;SimpleAsyncTaskExecutor&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;는 요청이 오는대로 계속해서 쓰레드를 생성한다.) 따라서 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;ThreadPoolTaskExecutor를 재정의함으로써 스레드 풀을 사용하도록 해야한다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;이것을 어플리케이션 레벨 또는 각 메서드 레벨에서 override 함으로써 default를 변경할 수 있다. 혹은 spring boot 2.0이상부터는 yaml파일로 설정이 가능하다.&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;222&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brma5t/btrWRDHOVJB/Q0Z1cUc8cxzxHqud5xYwO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brma5t/btrWRDHOVJB/Q0Z1cUc8cxzxHqud5xYwO1/img.png&quot; data-alt=&quot;application.yml&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brma5t/btrWRDHOVJB/Q0Z1cUc8cxzxHqud5xYwO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrma5t%2FbtrWRDHOVJB%2FQ0Z1cUc8cxzxHqud5xYwO1%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;222&quot; height=&quot;158&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;application.yml&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Async는 결국 BeanProcessor에 의해 Proxy객체로 반환되고 이를 사용시 Interceptor에 의해 Executor가 실행이 된다 ( 포인트컷은 @Async가 달린 클래스나 메서드 )&lt;/li&gt;
&lt;li&gt;위의 과정에 의해 Bean이 등록이 되어있어야 Async가 작동한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tempArticleService의존성을 제거해주고 &lt;u&gt;ApplicationEventPublisher&lt;/u&gt; 의존성을 추가해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XPLSb/btrWRC3e0vC/NrKD0nlcbahsDwBnf1lMSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XPLSb/btrWRC3e0vC/NrKD0nlcbahsDwBnf1lMSk/img.png&quot; data-alt=&quot;변경 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XPLSb/btrWRC3e0vC/NrKD0nlcbahsDwBnf1lMSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXPLSb%2FbtrWRC3e0vC%2FNrKD0nlcbahsDwBnf1lMSk%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;911&quot; height=&quot;279&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;변경 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EventHandler&lt;/h4&gt;
&lt;pre id=&quot;code_1676386372182&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Component
public class TempArticleEventHandler {

    private final TempArticleEventService tempArticleEventService;

    @Async
    @TransactionalEventListener
    public void deleteTempArticle(TempArticleEvent event) {
        tempArticleEventService.delete(event.getTempArticleId());
}&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;AsyncConfigurer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AsyncConfigurer 인터페이스를 구현하여 ThreadPoolTaskExecutor를 커스텀하여 사용할 수 있다. 이 Configurer는 뒤에 예외처리할 때도 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1676195523405&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
   
   	@Override
	public Executor getAsyncExecutor() {
		ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
		threadPoolTaskExecutor.setThreadNamePrefix(&quot;async-delete-tempArticle&quot;);
		threadPoolTaskExecutor.setCorePoolSize(2); // 스레드 풀에 속한 기본 스레드 갯수
		threadPoolTaskExecutor.setQueueCapacity(10); // 이벤트 대기 큐 (deafult가 integer.MAX)
		threadPoolTaskExecutor.setMaxPoolSize(10); // 최대 pool size
		threadPoolTaskExecutor.initialize();
		return threadPoolTaskExecutor;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CorePoolSize는 실행 상태의 스레드 최수 개수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 점유하고 있는 스레드가 corePoolSize만큼을 넘어서면 QueueCapacity크기만큼 큐에 담을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;maxPoolSize는 스레드 풀에서 사용할 수 있는 최대 thread 개수로 큐에도 꽉 차게 된다면 설정 값만큼 스레드 풀 사이즈를 늘린다.&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;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HIDGP/btrWgh58mAn/cTSYvcncPh2vAXZYaK9Kz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HIDGP/btrWgh58mAn/cTSYvcncPh2vAXZYaK9Kz1/img.png&quot; data-alt=&quot;공식 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HIDGP/btrWgh58mAn/cTSYvcncPh2vAXZYaK9Kz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHIDGP%2FbtrWgh58mAn%2FcTSYvcncPh2vAXZYaK9Kz1%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;1319&quot; height=&quot;224&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비동기 이벤트는 예외가 전파되지 않는다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;Spring also provides an&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;&lt;b&gt;AsyncResult&lt;/b&gt;&lt;/u&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;class that implements&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Future&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;. We can use this to track the result of asynchronous method execution.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;순차적인 결과를 반환하는 이벤트를 퍼블리싱 할 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1673943457382&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Async
public Future&amp;lt;String&amp;gt; asyncMethodWithReturnType() {
    System.out.println(&quot;Execute method asynchronously - &quot; 
      + Thread.currentThread().getName());
    try {
        Thread.sleep(5000);
        return new AsyncResult&amp;lt;String&amp;gt;(&quot;hello world !!!!&quot;);
    } catch (InterruptedException e) {
        //
    }

    return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;메소드의 리턴타입이 Future일 경우 예외처리는 쉽다 - Future.get() 메소드가 예외를 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 리턴타입이 void일 때, 예외는 호출 스레드에 전달되지 않을 것이다. 따라서 우리는 예외 처리를 위한 추가 설정이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;우리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;AsyncUncaughtExceptionHandler&lt;/span&gt;&lt;span&gt;&amp;nbsp;인터페이스를 구현함으로서 커스텀&amp;nbsp;비동기 예외처리자를 만들것이다.&amp;nbsp;&lt;/span&gt;&lt;span&gt;handleUncaughtException()&amp;nbsp;&lt;/span&gt;&lt;span&gt;메소드는 잡히지않은&lt;span&gt;uncaught&lt;/span&gt; 비동기 예외가 발생할때 호출된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1673943550988&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
 
    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
        System.out.println(&quot;Exception message - &quot; + throwable.getMessage());
        System.out.println(&quot;Method name - &quot; + method.getName());
        for (Object param : obj) {
            System.out.println(&quot;Parameter value - &quot; + param);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #535353;&quot;&gt;전 섹션에서 우리는 설정 클래스에 의해 구현된&amp;nbsp;&lt;/span&gt;&lt;span&gt;AsyncConfigurer&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #535353;&quot;&gt;&amp;nbsp;인터페이스를 보았다. 그 일부로서 우리의 커스텀 비동기 예외처리자를 리턴하는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #535353;&quot;&gt;&lt;span&gt;&lt;i&gt;getAsyncUncaughtExceptionHandler()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/i&gt;메소드 또한 오버라이드해주어야한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1673943562060&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기에서 예외처리는 스레드가 다르게 동작하므로 디비에서만으론 처리가 안되는 것 같다. 따라서 어플리케이션에서 따로 처리를 해주어야 한다고 생각했다. 그래서 A&amp;rarr;B 트랜잭션으로 비동기 일어날때,&amp;nbsp;3가지 경우를 생각해봤다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A가 성공 &amp;rArr; 이벤트 발행 &amp;rArr; B 실행 (성공)&lt;/li&gt;
&lt;li&gt;A가 실패 &amp;rArr; 이벤트 발행 X &amp;rArr; B 작동 X ( A는 롤백)&lt;/li&gt;
&lt;li&gt;A가 성공 &amp;rArr; 이벤트 발행 &amp;rArr; B 실패할 때, (B롤백 &amp;rArr; 비동기라서 쓰레드가 다르므로 예외가 전파가 안된다.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 3번의 예외 처리를 애플리케이션에서 처리해주어 만약 임시 게시글이 삭제가 안되었다면 -&amp;gt; 예외를 터뜨려 사용자에게 임시 게시글이 삭제가 안되었다고 알리는 방법이 있겠다. 혹은 해당 임시 게시글의 id를 모아 나중에 스케줄러를 통해서 삭제하는 방법이 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-async&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/spring-async&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>Async</category>
      <category>EVENT</category>
      <category>비동기</category>
      <category>트랜잭션</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/154</guid>
      <comments>https://giron.tistory.com/154#entry154comment</comments>
      <pubDate>Fri, 17 Feb 2023 20:32:39 +0900</pubDate>
    </item>
    <item>
      <title>Optional.orElse() vs .orElseGet()</title>
      <link>https://giron.tistory.com/153</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Java8에서 추가된 Optional은 null을 관리하기 편하게 해주는 객체이다. Optional을 사용하다 보면. orElse() 나 .orElseGet()을 사용한다. 해당 차이점에 대해서 경험한 내용을 적어보려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DPuQf/btrYARcKNIR/WPkuzUxFaePwHvRCxOwxU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DPuQf/btrYARcKNIR/WPkuzUxFaePwHvRCxOwxU0/img.png&quot; data-alt=&quot;orElse vs orElseGet&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DPuQf/btrYARcKNIR/WPkuzUxFaePwHvRCxOwxU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDPuQf%2FbtrYARcKNIR%2FWPkuzUxFaePwHvRCxOwxU0%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;1042&quot; height=&quot;399&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;orElse vs orElseGet&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;orElse()&lt;/h4&gt;
&lt;pre id=&quot;code_1675865572717&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Coupon coupon = couponRepository.findById(1L).orElse(new Coupon());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Id가 1인 Coupon이 있으면 해당 Coupon을 반환하고 없으면 new Coupon()을 반환하는 로직이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;orElseGet()&lt;/h4&gt;
&lt;pre id=&quot;code_1675865872406&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Coupon coupon = couponRepository.findById(1L).orElseGet(() -&amp;gt; new Coupon());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 람다를 통해 반환하는 모습을 볼 수있다. &lt;span style=&quot;color: #000000;&quot;&gt;즉, orElseGet()&amp;nbsp;메서드는&amp;nbsp;orElse()&amp;nbsp;메서드와 달리 런타임에 고정되지 않는 값을 지연 로딩(Lazy Loading)할 수 있다.&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: #000000;&quot;&gt;그런데, 여기서 아래 예시를 보자. 만약 아래처럼 &lt;u&gt;orElse&lt;/u&gt;를 사용하고 내부에 함수를 호출하면 쿠폰이 &lt;u&gt;null이 아니어도&lt;/u&gt; giveWelcomeCoupon() &lt;u&gt;함수를 호출&lt;/u&gt;하게 된다. 반면에 orElseGet()을 사용하면 null일 때만 정상적으로 호출이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1675867377156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class OptionalTest{
	@DisplayName(&quot;null이 아닐 때, Optional.orElse(), orElseGet() 테스트&quot;)
    @Test
    void Optional() {
        Coupon coupon = new Coupon(&quot;hello&quot;);
        Coupon elseCoupon = Optional.of(coupon).orElse(giveWelcomeCoupon());
        System.out.println(elseCoupon);

        Coupon elseGetCoupon = Optional.of(coupon).orElseGet(this::giveWelcomeCoupon);
        System.out.println(elseGetCoupon);
    }

    private Coupon giveWelcomeCoupon() {
        System.out.println(&quot;welcome!!&quot;);
        return new Coupon(&quot;welcomeCoupon&quot;);
    }
}&lt;/code&gt;&lt;/pre&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;242&quot; data-origin-height=&quot;95&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0gU1Q/btrYwobYZEj/FJLQmB6LM7GEC9rJCtsLBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0gU1Q/btrYwobYZEj/FJLQmB6LM7GEC9rJCtsLBK/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0gU1Q/btrYwobYZEj/FJLQmB6LM7GEC9rJCtsLBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0gU1Q%2FbtrYwobYZEj%2FFJLQmB6LM7GEC9rJCtsLBK%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;242&quot; height=&quot;95&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;95&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 로직은 동작하지만 &lt;u&gt;elseCoupon&lt;/u&gt;이 &lt;b&gt;&quot;welcomeCoupon&quot;&lt;/b&gt;으로 반환되지는 않는 것을 확인할 수 있다. 중요한 것은 &lt;u&gt;&lt;b&gt;내부적으로 giveWelcomeCoupon() 메서드를 탔다는 것이다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어째서 이런일이 발생하는 것일까?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;orElse()에서 null이 아니어도 호출되는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 위의 orElse()가 &lt;u&gt;메서드&lt;/u&gt;를 호출하기 때문이다. 만약 아래처럼 바로 객체를 호출하면 어떻게 될까?&lt;/p&gt;
&lt;pre id=&quot;code_1675868096451&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class OptionalTest{
	@DisplayName(&quot;null이 아닐 때, Optional.orElse(), orElseGet() 테스트&quot;)
    @Test
    void Optional() {
        Coupon coupon = new Coupon(&quot;hello&quot;);
        Coupon elseCoupon = Optional.of(coupon).orElse(new Coupon(&quot;HELLO COUPON!!!&quot;)); // 메서드 호출 X
        System.out.println(elseCoupon);

        Coupon elseGetCoupon = Optional.of(coupon).orElseGet(this::giveWelcomeCoupon);
        System.out.println(elseGetCoupon);
    }

    private Coupon giveWelcomeCoupon() {
        System.out.println(&quot;welcome!!&quot;);
        return new Coupon(&quot;welcomeCoupon&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 생각했던대로 orElse()와 orElseGet() 모두 호출되지 않았다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;241&quot; data-origin-height=&quot;60&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GwYhh/btrYz9EKHPq/sRwHdQetBk9JK6LO9J8xL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GwYhh/btrYz9EKHPq/sRwHdQetBk9JK6LO9J8xL0/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GwYhh/btrYz9EKHPq/sRwHdQetBk9JK6LO9J8xL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGwYhh%2FbtrYz9EKHPq%2FsRwHdQetBk9JK6LO9J8xL0%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;241&quot; height=&quot;60&quot; data-origin-width=&quot;241&quot; data-origin-height=&quot;60&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;orElse() 메서드 내부를 를 다시 보자. giveWelcomeCoupon()이라는 함수가 들어오면 아래처럼 될 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1675866701408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; public T orElse(giveWelcomeCoupon()) {
        return value != null ? value : giveWelcomeCoupon();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환타입이 T타입이어야 하기 때문에 giveWelcomeCoupon()이 동작할 것이다. 따라서 내부적으로 메서드가 동작을 한다.&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;그렇다면 orElseGet()을 살펴보자&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675869260306&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public T orElseGet(Supplier&amp;lt;? extends T&amp;gt; supplier) {
        return value != null ? value : supplier.get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드를 바로 실행하는 것이 아닌, null일 때, get()을 실행하기 때문에 바로 로직이 타지는 것이 아니었다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.orElse()와 .orElseGet()은 null이든 아니든 모두 인자로 받은 값들을 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.orElse()를 사용할 때, 메서드를 인자로 가지면 null이 아니어도 호출이 된다! 하지만 반환값은 호출한 메서드의 반환값으로 바뀌지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.orElseGet()은 null일 때만 호출이 된다! 또한 supply()를 사용하므로 Lazy-evaluation으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 성능상으로도 .orElseGet()이 조금 앞선다. (매번 호출하지 않아도 되기 때문이다.)&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/java-optional-or-else-vs-or-else-get&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/java-optional-or-else-vs-or-else-get&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/자바</category>
      <category>.orElse()</category>
      <category>.orElseGet()</category>
      <category>lazy-evalutation</category>
      <category>optional</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/153</guid>
      <comments>https://giron.tistory.com/153#entry153comment</comments>
      <pubDate>Fri, 10 Feb 2023 14:24:51 +0900</pubDate>
    </item>
    <item>
      <title>[Collection Framework] Map, Set, List 중 Map에 대해서 알아보자</title>
      <link>https://giron.tistory.com/152</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdNayt/btrTMfCikGD/cLdlTKzMZVLv2hc88ih5A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdNayt/btrTMfCikGD/cLdlTKzMZVLv2hc88ih5A1/img.png&quot; data-alt=&quot;collection 상속 관계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdNayt/btrTMfCikGD/cLdlTKzMZVLv2hc88ih5A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdNayt%2FbtrTMfCikGD%2FcLdlTKzMZVLv2hc88ih5A1%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;546&quot; height=&quot;218&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;collection 상속 관계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바는 최상위에 Collection Framework를 가진다. 크게 Collection Framework하위에 &lt;b&gt;&lt;u&gt;Map&lt;/u&gt;&lt;/b&gt;과 &lt;b&gt;&lt;u&gt;Collection인터페이스&lt;/u&gt;&lt;/b&gt;로 나뉜다. &lt;span style=&quot;color: #000000;&quot;&gt;컬렉션 프레임워크는 컬렉션을 표현하고 조작하기 위한 통합 아키텍처로, 컬렉션이 구현 세부 사항과 독립적으로 조작될 수 있도록 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 컬렉션 프레임워크 덕분에 데이터 구조와 알고리즘을 제공하여 직접 작성할 필요가 없는 장점이 있습니다. 또한 컬렉션 인터페이스를 구현함으로써 List, Set, Queue에서는 동일한 api를 사용합니다. ex) add(), clear()...&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;Map
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key와 Value의 형태로 이루어진 데이터 집합&lt;/li&gt;
&lt;li&gt;순서를 보장하지 않는다.&lt;/li&gt;
&lt;li&gt;Key는 중복이 허용되지 않고, Value는 중복을 허용한다.&lt;/li&gt;
&lt;li&gt;내부적으로 &lt;u&gt;링크드리스트, 배열&lt;/u&gt;(버킷)로 구현되어있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Collection 하위 인터페이스&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;List
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순서가 있는 데이터 집합&lt;/li&gt;
&lt;li&gt;데이터를 중복해서 포함시킬 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터의 중복을 허용하지 않는다.&lt;/li&gt;
&lt;li&gt;순서를 보장하지 않는다.&lt;/li&gt;
&lt;li&gt;내부적으로&lt;u&gt; HashMap&lt;/u&gt;으로 구현되어있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Concurrent Collections&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 문제를 해결하기 위해 지원해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BlockingQueue,TransferQueue, BlockingDeque,ConcurrentMap 등 다양한 인터페이스가 있다. 그리고 LinkedBlockingQueue, ArrayBlockingQueue, ConcurrentHashMap등 다양한 구현 클래스가 있다.&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;이렇게 설계한 목표는 최대한 작은 core 인터페이스만 만드려고 했다고 합니다. 추후에 추가될 기능의 교체가 아닌 증설을 목적으로 개념적 가중치가 있는 API를 만드려고 CollectionFramework를 디자인했다고 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Map&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map은 key, value 쌍으로 이루어진 대표적인 자료구조입니다. 자바에서 사용하는 Map은 내부적으로 배열로 이루어져있습니다. key값이 들어오면 이 값을 해싱을 합니다. 해싱한 값을 배열의 인덱스로 하고 value를 해당 배열(버킷)의 인덱스 위치에 저장합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 해시값을 이용하기때문에 해시 충돌이 발생할 수 있습니다. 해시 충돌을 해결하는 방법으로는 크게 &lt;span style=&quot;color: #555555;&quot;&gt;&lt;u&gt;Open Addressing&lt;/u&gt;과 &lt;u&gt;Separate Chaining&lt;/u&gt; 방식이 있습니다. 이중 자바의 Map은 &lt;u&gt;Separate Chaining&lt;/u&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span&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: #555555;&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span&gt;OpenAddressing은 해시 충돌이 발생하면 다른 버킷 위치에 해당 value를 넣어서 해결합니다. 이러한 방법은 &lt;span style=&quot;color: #555555;&quot;&gt;연속된 공간에 데이터를 저장하기 때문에 Separate Chaining에 비하여 캐시 효율이 높습니다. 따라서 &lt;u&gt;데이터 개수가 충분히 적다면 Open Addressing이 Separate Chaining보다 더 성능이 좋다.&lt;/u&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #555555;&quot;&gt;하지만 Java의 HashMap이 &lt;u&gt;&lt;u&gt;Separate&lt;/u&gt;&lt;u&gt;Chaining&lt;/u&gt;&amp;nbsp;&lt;/u&gt;을 사용하는 이유는 remove에 관련이 있다. OpenAddressing은 key를 삭제한 후 다시 정렬해야 하는 추가 작업이 필요합니다. 따라서 &lt;span style=&quot;color: #555555;&quot;&gt;자바의 Map은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;Separate Chaining&lt;/u&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 사용합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/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: #555555;&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #555555;&quot;&gt;이때 자바의 HashMap은 특징이 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ePTDtP/btrTKrjq4Oz/nm0AnVlfLcxCTtsd7s0yQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ePTDtP/btrTKrjq4Oz/nm0AnVlfLcxCTtsd7s0yQK/img.png&quot; data-alt=&quot;6과 8&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ePTDtP/btrTKrjq4Oz/nm0AnVlfLcxCTtsd7s0yQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FePTDtP%2FbtrTKrjq4Oz%2Fnm0AnVlfLcxCTtsd7s0yQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;265&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;6과 8&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 충돌이 발생해서 링크드 리스트가 8개 이상이 되는 순간 링크드 리스트가 Tree형태로 변환이 됩니다. 이때 사용되는 트리는 RBTree라고 합니다. RedBlackTree는 최악의 경우도 O(logn)으로 유지할 수 있는 트리입니다. 따라서 충돌 데이터가 너무 많다면 트리 형태로 변환해서 탐색 속도를 높인 것입니다. 마찬가지로 6개가 되면 다시 리스트 형태로 변환됩니다. 2의 차이를 둔 이유는 -,+반복으로 매번 자료구조가 변하면 비용이 들기 때문에 차이를 2로 두었다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8을 기준으로 한 이유는 &lt;span style=&quot;color: #555555;&quot;&gt;트리는 링크드 리스트보다 메모리 사용량이 많고, 데이터의 개수가 적을 때 트리와 링크드 리스트의 Worst Case 수행 시간 차이 비교는 의미가 없기 때문에 8로 설정한것 같습니다.&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;span style=&quot;color: #555555;&quot;&gt;버킷의 확장&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;해시 충돌을 &lt;u&gt;Separate Chaining&lt;/u&gt;으로 해결해준다면 버킷은 언제 확장할까요?&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;static final int DEFAULT_INITIAL_CAPACITY = 1 &amp;lt;&amp;lt; 4; // aka 16&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 버킷은 기본값이 16입니다. 따라서 해당 버킷의 3/4(0.75)가 차게 된다면 2배로 사이즈를 늘리는 작업을 합니다. 이렇게 되면 체이닝도 다시 설정되어야 하니 성능이 안 좋아질 수 있습니다. 따라서 어느 정도 크기가 예상이 된다면 미리 버킷의 크기를 잡아주는 것도 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;HashMap은 보조 해시 함수(Additional Hash Function)를 사용하기 때문에 보조 해시 함수를 사용하지 않는 HashTable에 비하여 해시 충돌(hash collision)이 덜 발생할 수 있어 상대적으로 성능상 이점이 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;보조 해시 함수는 %(모듈러)연산을 했을 때, 고르지 않게 나오는 문제를 해결해준다. 예를 들어 해시 버킷이 2배씩 확장하므로 2의 지수승으로 확장되므로 %연산으로 적극 2^30 범위를 확보하지 못한다. 2^a로 모듈러하면 a비트만 활용하기 때문이다. (사실 31과 같은 소수를 활용하면 더 다양한 비트를 활용할 수 있을 것이다.)&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: #555555;&quot;&gt;따라서 보조 해시 함수를 통해서 해결한다.&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UoALW/btrTK7kv5Fi/UBAJxeK2EGrhuC6KdHxee1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UoALW/btrTK7kv5Fi/UBAJxeK2EGrhuC6KdHxee1/img.png&quot; data-alt=&quot;보조해시함수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UoALW/btrTK7kv5Fi/UBAJxeK2EGrhuC6KdHxee1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUoALW%2FbtrTK7kv5Fi%2FUBAJxeK2EGrhuC6KdHxee1%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;561&quot; height=&quot;93&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;보조해시함수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;xor연산을 통해서 해결을 한다. java docs에 설명이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don't benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;대부분의 해시의 집합들은 이미 합리적으로 분배가 되었다.(따라서 비트를 조금만 사용한다고 해도 해시 충돌이 발생할 확률이 줄었다.) 게다가 해시충돌을 tree로 다루기 때문이다. 따라서 해싱한 값에 대해서 xor 같은 비트 연산을 통해서 빠르게 연산하여 수행 속도를 상승시키는 목적으로 보조 해시함수를 사용한다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Map&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 Map도 5가지로 구현됩니다. HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap, HashTable&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashMap&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized가 안 걸려있어서 thread-safe하지 않습니다.&lt;/li&gt;
&lt;li&gt;null을 허용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TreeMap&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized가 안 걸려있어서 thread-safe하지 않습니다.&lt;/li&gt;
&lt;li&gt;default로 key값을 기준으로 오름차순으로 정렬이됩니다.(Comparator를 구현하여 정렬방법을 설정할 수 있습니다.)&lt;/li&gt;
&lt;li&gt;null을 허용하지 않습니다.&lt;/li&gt;
&lt;li&gt;red-black tree로 구현되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LinkedHashMap&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized가 안 걸려있어서 thread-safe하지 않습니다.&lt;/li&gt;
&lt;li&gt;입력 순서가 보장이 됩니다.&lt;/li&gt;
&lt;li&gt;null을 허용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentHashMap&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;put에서 내부적으로 synchronized가 걸려있어서 동시성을 보장해줍니다. 같은 버킷에 접근하는 로직에만 synchronized를 걸어서 해결해줍니다.&lt;/li&gt;
&lt;li&gt;null을 허용하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashTable&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized가 get, put모두에 걸려있어서 성능상 좋지 않습니다.&lt;/li&gt;
&lt;li&gt;null을 허용하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Set&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속도는 HashSet = LinkedHashSet &amp;gt; TreeSet로 HashSet과 LinkedHashSet은 비슷합니다. 하지만 TreeSet은 매번 정렬해야 하므로 느립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashSet&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;Null 저장 가능&lt;/li&gt;
&lt;li&gt;해시 알고리즘을 사용하여 검색속도가 매우 빠르다.&lt;/li&gt;
&lt;li&gt;내부적으로 HashMap으로 구현되어 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LinkedHashSet&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장 순서를 유지하는 HashSet&lt;/li&gt;
&lt;li&gt;내부적으로 LinkedHashMap으로 구현되어 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TreeSet&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 정렬된 상태로 저장되는 이진 탐색 트리의 형태로 요소를 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Null 저장 불가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레드 블랙 트리(균형 이진 탐색 트리)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 구현되어 있다.&lt;/li&gt;
&lt;li&gt;Compartor 구현으로 정렬 방법을 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;내부적으로 TreeMap으로 구현되어 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;List&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ArrayList
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부적으로 배열을 사용하는 자료구조로 메모리가 연속적으로 배치된다.&lt;/li&gt;
&lt;li&gt;배열과 달리 메모리 할당이 동적이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;배열은 Compile time에 할당되어 힙에 저장되는 정적 메모리 할당&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;리스트는 새로운 Node가 추가되고 runtime에 할당되어 힙에 저장되는 동적 메모리 할당&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 삽입, 삭제 시 해당 데이터 이후 모든 데이터가 복사되므로 빈번한 삭제, 삽입이 일어나는 경우에는 부적합하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재할당 시 크기의 절반씩 증가한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LinkedList
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;양방향 포인터 구조로 각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 방식으로 데이터를 저장하는 자료 구조&lt;/li&gt;
&lt;li&gt;데이터의 삽입, 삭제 시 해당 노드의 주소지만 바꾸면 되므로 삽입, 삭제가 빈번한 데이터에 적합하다.&lt;/li&gt;
&lt;li&gt;메모리가 불연속적이다.&lt;/li&gt;
&lt;li&gt;데이터 검색 시 처음부터 순회하므로 검색에는 부적합하다.&lt;/li&gt;
&lt;li&gt;스택, 큐, 양방향 큐를 만들기 위한 용도로 사용한다.&lt;/li&gt;
&lt;li&gt;양옆의 정보만을 갖고 있기 때문에 순차적으로 검색을 진행하여 검색 속도가 느리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/technotes/guides/collections/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.oracle.com/javase/8/docs/technotes/guides/collections/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/831311&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://d2.naver.com/helloworld/831311&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/자바</category>
      <category>Collection</category>
      <category>List</category>
      <category>map</category>
      <category>set</category>
      <category>자료구조</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/152</guid>
      <comments>https://giron.tistory.com/152#entry152comment</comments>
      <pubDate>Tue, 20 Dec 2022 17:10:31 +0900</pubDate>
    </item>
    <item>
      <title>[Transaction] commit된 트랜잭션에 롤백된 트랜잭션이 참여하면 어떻게 될까? TranscationalEventListener와 함께 알아보자</title>
      <link>https://giron.tistory.com/151</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 application context는 Application Event를 제공해준다. 해당 이벤트를 사용할 때 기존 트랜잭션에서 커밋이 된 후에 이벤트를 처리할 때, Transaction의 Propagation을 Requires_new 나 @Asnyc로 주지 않으면 같은 트랜잭션으로 묶여서 정상적으로 이벤트가 발급되지 않는다. 따라서 이벤트에 Propagation.REQUIRES_NEW 옵션을 주어서 사용했는데 이때 Requires_new를 사용한다고 해도 상위 트랜잭션에서 예외를 잡아주지 않으면 예외가 전파되어서 상위 트랜잭션도 롤백이 된다. 따라서 try-catch로 잡아줘야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데.. 실제 event에 대해서 예외 테스트를 진행해줬는데 try-catch로 잡지 않아도 예외가 전파되지 않고 상위 트랜잭션은 커밋이 되었다. 어떻게 발생한 일인 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 추측으로는 TransactionEventListener는 트랜잭션이 커밋된 이후에 이벤트를 publish 해준다. 즉, 상위 트랜잭션에서 &lt;b&gt;커밋 마킹&lt;/b&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;한 번 실습을 해보면서 알아보자!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비즈니스 상황 가정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;우리의 서비스가 Point를 사용하면 사용자에게 문자를 발급해주는 서비스라고 가정하자.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;span&gt;#PointService&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Point를 사용할 때 이벤트를 발생시키는 로직이다.&lt;/p&gt;
&lt;pre id=&quot;code_1669908381819&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PointService {
	...
    
	public void use(PointRequest request) {
        
       	...
        
        eventPublisher.publishEvent(new SmsEvent(user.getName(), point.getAmount()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;#SmsEventListener&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Point를 사용할 때 발생한 이벤트를 받아 처리를 하는 메서드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1669908631606&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class SmsEventListener {
	
    ...
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionEventListener
    public void send(SmsEvent event) {
    	smsService.send(event);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;#Test code&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 5천 포인트가 있고 100포인트를 사용하면 포인트는 정상적으로 차감이 되고 sms 기록에 저장이 되었는지 확인하는 테스트입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1669908996900&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PointTest {
	@Transactional
 	@Test
    void rollback() {
        pointRepository.save(new Point(5000L)); // 초기에 5천 포인트

        //when
        pointService.use(100L); // 100 포인트 사용
        Point point = pointRepository.findAll().get(0);
        TestTransaction.flagForCommit();
        TestTransaction.end();
        TestTransaction.start();

        List&amp;lt;Sms&amp;gt; smss = smsRepository.findAll();
        
        //then
        assertAll(
                () -&amp;gt; assertThat(point.getAmount()).isEqualTo(4900), // 5000- 100
                () -&amp;gt; assertThat(smss).hasSize(1)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EventListener + propagation.REQUIRES_NEW&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 추측이 맞다면 트랜잭션이 커밋하기 전에 바로 이벤트를 publish 해주는 @EventListener를 사용하면 테스트에서 정상적으로 예외가 발생해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 IllgealStateExcpetion을 발생시키면 상위 트랜잭션도 롤백이 되어야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;#SmsEventListener 수정&lt;/h4&gt;
&lt;pre id=&quot;code_1669907807196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class SmsEventListener {
	
    ...
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @EventListener
    public void send(SmsEvent event) {
    	throw new IllegalStateException(&quot;REQUIRES_NEW 이벤트 예외 발생&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예상대로 예외 발생&lt;/h4&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;643&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXkRl3/btrSCMaSUTH/dPkp7Y7j9ce5qD4lJep4Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXkRl3/btrSCMaSUTH/dPkp7Y7j9ce5qD4lJep4Yk/img.png&quot; data-alt=&quot;예외 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXkRl3/btrSCMaSUTH/dPkp7Y7j9ce5qD4lJep4Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXkRl3%2FbtrSCMaSUTH%2FdPkp7Y7j9ce5qD4lJep4Yk%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;643&quot; height=&quot;68&quot; data-origin-width=&quot;643&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예외 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 처음 가설대로 트랜잭션에 커밋 마크가 먼저 붙으면 하위 트랜잭션에서 롤백이 일어나도 정상적으로 커밋이 된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 다시 @TransactionEventListener를 적용해서 테스트를 해보면 아래처럼 2번째 트랜잭션만 롤백이 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1073&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqP6KX/btrSBDeIq05/FF4MXLpv9sdSHvCJKS81d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqP6KX/btrSBDeIq05/FF4MXLpv9sdSHvCJKS81d1/img.png&quot; data-alt=&quot;트랜잭션 로그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqP6KX/btrSBDeIq05/FF4MXLpv9sdSHvCJKS81d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqP6KX%2FbtrSBDeIq05%2FFF4MXLpv9sdSHvCJKS81d1%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;1073&quot; height=&quot;339&quot; data-origin-width=&quot;1073&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;트랜잭션 로그&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이번엔 코드를 통해서 알아보자. 어떤 구조이길래 이런 일이 발생할 까?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;디버깅&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1325&quot; data-origin-height=&quot;575&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWAPfm/btrSDJkt65W/cSkG4Hrx9IBdRcR3wBDRJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWAPfm/btrSDJkt65W/cSkG4Hrx9IBdRcR3wBDRJ1/img.png&quot; data-alt=&quot;깨끗하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWAPfm/btrSDJkt65W/cSkG4Hrx9IBdRcR3wBDRJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWAPfm%2FbtrSDJkt65W%2FcSkG4Hrx9IBdRcR3wBDRJ1%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;1325&quot; height=&quot;575&quot; data-origin-width=&quot;1325&quot; data-origin-height=&quot;575&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;깨끗하다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;#AbstractPlatformTransactionManager.java&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DUZBv/btrSCsjryt1/x6mC00LS9yGPjXUMuFXQ6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DUZBv/btrSCsjryt1/x6mC00LS9yGPjXUMuFXQ6K/img.png&quot; data-alt=&quot;status.iscompleted()일 때,&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DUZBv/btrSCsjryt1/x6mC00LS9yGPjXUMuFXQ6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDUZBv%2FbtrSCsjryt1%2Fx6mC00LS9yGPjXUMuFXQ6K%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;1236&quot; height=&quot;342&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;status.iscompleted()일 때,&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 트랜잭션이 이미 종료되었으면 예외를 발생한다. 즉, 이벤트는 커밋이 되었어도 트랜잭션이 종료되어있지 않다는 말은 사실이었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;#AbstractPlatformTransactionManager.java 이어서&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1321&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5nxMx/btrSz8lNgF7/R7ucPAd5jTiJV5DX8RSufK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5nxMx/btrSz8lNgF7/R7ucPAd5jTiJV5DX8RSufK/img.png&quot; data-alt=&quot;실제 커밋이 발생하는 로직이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5nxMx/btrSz8lNgF7/R7ucPAd5jTiJV5DX8RSufK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5nxMx%2FbtrSz8lNgF7%2FR7ucPAd5jTiJV5DX8RSufK%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;1321&quot; height=&quot;553&quot; data-origin-width=&quot;1321&quot; data-origin-height=&quot;553&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 커밋이 발생하는 로직이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOmmp9/btrSCGaIdox/GwfSNqnlQ358DCkAmu2xLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOmmp9/btrSCGaIdox/GwfSNqnlQ358DCkAmu2xLK/img.png&quot; data-alt=&quot;자세히&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOmmp9/btrSCGaIdox/GwfSNqnlQ358DCkAmu2xLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOmmp9%2FbtrSCGaIdox%2FGwfSNqnlQ358DCkAmu2xLK%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;1214&quot; height=&quot;784&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자세히&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;# TransactionImpl&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nyhIN/btrSCKjReRP/UdE3GgYFd4WqYKAdx91TSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nyhIN/btrSCKjReRP/UdE3GgYFd4WqYKAdx91TSk/img.png&quot; data-alt=&quot;진짜찐짜 커밋&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nyhIN/btrSCKjReRP/UdE3GgYFd4WqYKAdx91TSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnyhIN%2FbtrSCKjReRP%2FUdE3GgYFd4WqYKAdx91TSk%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;890&quot; height=&quot;384&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;진짜찐짜 커밋&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lwLSu/btrSE7kRf4R/wSJ5RMCEInRkUqxFL8OiTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lwLSu/btrSE7kRf4R/wSJ5RMCEInRkUqxFL8OiTk/img.png&quot; data-alt=&quot;롤백 마크를 해도 isActive true일 수가 있다?!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lwLSu/btrSE7kRf4R/wSJ5RMCEInRkUqxFL8OiTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlwLSu%2FbtrSE7kRf4R%2FwSJ5RMCEInRkUqxFL8OiTk%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;1086&quot; height=&quot;389&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;롤백 마크를 해도 isActive true일 수가 있다?!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 살아있는 상태인지 계속해서 체크하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;#AbstractPlatformTransactionManager.java 이어서2&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;997&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVM2yk/btrSEfXIa8s/3HfRkrf8Qz2e8nHY0UO4K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVM2yk/btrSEfXIa8s/3HfRkrf8Qz2e8nHY0UO4K1/img.png&quot; data-alt=&quot;진짜 진짜 진짜 커밋&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVM2yk/btrSEfXIa8s/3HfRkrf8Qz2e8nHY0UO4K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVM2yk%2FbtrSEfXIa8s%2F3HfRkrf8Qz2e8nHY0UO4K1%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;997&quot; height=&quot;802&quot; data-origin-width=&quot;997&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;진짜 진짜 진짜 커밋&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 커밋을 합니다. 흐름을 보면 Active 한 상태인지 확인을 계속하면서 롤백 마크가 있는지 확인을 합니다. 만약 롤백 마크도 없고 active 한 상태라면 최종적으로 commit을 진행하고 COMMIT마크를 남깁니다. 이미 커밋이 되었고 커밋 마크도 남겼으니 예외가 터졌다고 해도 변경에 영향이 없던 것입니다. 생각해보면 당연한 부분이었군요. &amp;zwj;♂️&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 처음 적용해보면서 재밌는 사실들을 많이 배웠습니다. 트랜잭션이 커밋이 되거나 롤백이 되어도 트랜잭션은 종료되지 않고 &lt;span style=&quot;background-color: #ffffff; color: #474747;&quot;&gt;active 하고 accessible&lt;/span&gt; 할 수 있다는 것을 알았습니다.(생각해보니 직접 트랜잭션 구현할 때도, commit하고 close했던 것을 생각해보면 당연히 commit과 트랜잭션 종료는 별개네요..) 하지만 이미 커밋 or 롤백이 되었기 때문에 새로운 변경을 커밋할 수는 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 @TransactionalEventListener을 사용할때는 try-catch를 안 하고도 새로운 트랜잭션에서 발생하는 예외에 영향을 받지 않을 수 있습니다.&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;980&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FRcIG/btrSEh2kPU7/1Jv8ZSKNpgUKs7kTMUdjkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FRcIG/btrSEh2kPU7/1Jv8ZSKNpgUKs7kTMUdjkK/img.png&quot; data-alt=&quot;추가 학습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FRcIG/btrSEh2kPU7/1Jv8ZSKNpgUKs7kTMUdjkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFRcIG%2FbtrSEh2kPU7%2F1Jv8ZSKNpgUKs7kTMUdjkK%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;980&quot; height=&quot;184&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;추가 학습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 타고 들어가다 보니 commit이 되어도 해당 메서드를 지나간다. global rollback-only가 적용되면 로컬에서 커밋이 일어나도 롤백이 발생하는 것 같다. 추후에 알아보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-events#transaction-bound-events&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/spring-events#transaction-bound-events&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/스프링부트</category>
      <category>propagation</category>
      <category>requires_new</category>
      <category>롤백</category>
      <category>이벤트</category>
      <category>전파</category>
      <category>트랜잭션</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/151</guid>
      <comments>https://giron.tistory.com/151#entry151comment</comments>
      <pubDate>Fri, 2 Dec 2022 02:30:41 +0900</pubDate>
    </item>
    <item>
      <title>OWASP로 알아보는 개발 생태계</title>
      <link>https://giron.tistory.com/150</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스에서 보안 특강을 듣고 정리가 필요하겠다고 생각했습니다. 백엔드 개발자도 OWASP정도는 알고 있어야 한다고 생각합니다. 따라서 정리를 해보겠습니다 &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OWASP란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666;&quot;&gt;OWASP는 &amp;lsquo;&lt;b&gt;O&lt;/b&gt;pen &lt;b&gt;W&lt;/b&gt;eb &lt;b&gt;A&lt;/b&gt;pplication &lt;b&gt;S&lt;/b&gt;ecurity &lt;b&gt;P&lt;/b&gt;roject&amp;rsquo;라는 비영리 보안 프로젝트 재단을 의미하는 약자입니다. 이 재단에서는 애플리케이션에서 발생할 수 있는 취약점을 분석하고 연구하고 있으며 3~4년 주기로 Top10을 발표합니다.&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;2021년 이슈&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZQEEW/btrRpYbB9GK/fThZDNZEgirz0GEdyvBerk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZQEEW/btrRpYbB9GK/fThZDNZEgirz0GEdyvBerk/img.png&quot; data-alt=&quot;owasp.org&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZQEEW/btrRpYbB9GK/fThZDNZEgirz0GEdyvBerk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZQEEW%2FbtrRpYbB9GK%2FfThZDNZEgirz0GEdyvBerk%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;1123&quot; height=&quot;323&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;owasp.org&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Injection(1st -&amp;gt; 3rd)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2017년까지 1위를 달리던 Injection 취약점이 21년부터는 3위로 내려온 모습을 볼 수 있습니다. 이러한 이유에 대해서는 ORM이 많이 쓰이면서 발생한 현상이라고 생각해볼 수 있겠네요. 왜냐하면 ORM을 사용하면 내부적으로 sql-Injection위험에 대해서 자동으로 회피해주기 때문이죠. 더 자세한 건 &lt;a href=&quot;https://giron.tistory.com/142&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;sql-Injection자세히 보기&lt;/a&gt;&amp;nbsp;여기서 볼 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Broken Access Control(5th -&amp;gt; 1st)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 주목할 점은 Broken Access Control문제입니다. 해당 취약점이 1위가 된 이유는 특강에서 듣기로는 토큰을 사용한 인증이 많아졌기 때문이라고 추측하셨습니다. 실제 해당 취약점은 클라이언트 단에서 인증 토큰 등을 관리하기 때문에 발생하는 문제입니다. 예를 들어, GET요청에서 url을 수정하여 admin같은 url을 넣어서 admin권한으로 공격하는 것이 있습니다. 이외에도 파라미터나 쿠키 등의 요청을 조작해 권한 상승 혹은 타 사용자의 권한을 사용할 수 있는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 예방하기 위해서 &lt;b&gt;검증은 반드시 서버에서 수행&lt;/b&gt;해야 합니다. 클라이언트에 있는 정보는 어떻게 해서든 탈취할 수 있다고 하는군요!&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Insecure Deserialization(8th -&amp;gt;8th): Software and Data Integrity Failures로 병합&lt;span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 병합되어 추가된 내용입니다. 기존에 역직렬화 취약점이 &lt;b&gt;데이터 무결성 실패&lt;/b&gt;로 병합이 되었습니다. 간략히 설명하자면 객체를 직렬화하면 바이트코드 형태로 저장이 됩니다. 이때 악성코드를 추가하여 바이트코드를 조작합니다. 이제 해당 바이트코드를 역직렬화하면 악성코드가 감염이 됩니다. 예시로는 아래와 같이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션이 사용하는 라이브러리나 모듈에 대한 무결성 검증이 없어 변조가 가능한 경우&lt;/li&gt;
&lt;li&gt;업데이트 공급망에 대한 검증이 없는 경우&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인에 대한 적절한 보안성 검토가 없는 경우&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;h4 data-ke-size=&quot;size20&quot;&gt;Server-Side-Request-Forgery( -&amp;gt; 10th)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 top10에 등장한 SSRF공격입니다. SSRF공격은 &lt;b&gt;서버단에서 요청을 발생시켜 내부 시스템에 접근하는 공격&lt;/b&gt;으로 공격법은 어렵지만 위력은 그만큼 강력하다고 알려져 있습니다. Public Cloud환경에서는 가상 서버의 메타데이터를 확인할 수 있는 API가 있어, SSRF를 통해 해당 API를 요청하여 가상 서버의 정보를 확인할 수 있습니다. 또한 내부 서버(게이트웨이)에 redirect url을 넣은 요청을 보내서 내부 서버에 접근할 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제도 마찬가지로 &lt;b&gt;검증이 이루어지지 않고 바로 요청을 받아들이기 때문에&lt;/b&gt; 발생합니다. 예방 방법으로는 화이트리스트 방식으로 접근 가능한 url만 열어 놓습니다. 그리고 Localhost, 127.0.0.1 등의 Loopback URI가 들어오면 필터링을 합니다. 또한 불필요한 특수문자 @와 같은 것들이 들어와도 필터링합니다. (공격하는 명령어 중에 @같은 특수문자가 필요했던.. 것 같습니다(가물가물하네요..))&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSRF가 새롭게 top 10으로 올라온 이유로는 아무래도 MSA를 많이 채택하면서 서버간의 소통이 더욱 많아져서 새롭게 올라오지 않았나..라고 조심스럽게 추측해 봅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Secure Coding&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시큐어 코딩은 이런 보안 문제를 인지하고 보안 관련 규정을 체크하면서 코딩하는 것입니다. 정부에서 이러한 secure coding가이드를 내려줍니다. 위의 보안 문제를 막기 위해서는 애초에 코딩할 때, 보안에 취약점이 최대한 생기지 않도록 안전 코딩을 하도록 노력해야겠습니다. 예를 들면 https설정을 했는가?, 응답, 오류 메시지 등에 was나 db정보가 노출되지 않았는가? 세션/토큰의 발급, 갱신, 만료 로직 등이 있는가 와 같은 것들이 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상 OWASP Top10중 몇몇 이슈들만 확인해봤습니다. 이외에도 XSS, CSRF 등 개발하면서 만난 여러 보안 이슈들이 있었는데 해당 글에 점진적으로 추가하려고 합니다. OWASP를 학습하다 보니 개발 생태계가 어떻게 변하고 있는지 확인도 할 수 있어서 재밌었네요. 앞으로도 계속 찾아볼 것 같네요 &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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://owasp.org/www-project-top-ten/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://owasp.org/www-project-top-ten/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <category>OWASP</category>
      <category>secure coding</category>
      <category>ssrf</category>
      <category>보안</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/150</guid>
      <comments>https://giron.tistory.com/150#entry150comment</comments>
      <pubDate>Thu, 17 Nov 2022 00:41:46 +0900</pubDate>
    </item>
    <item>
      <title>[테스트 자동화 2] @AfterEachCallBack을 통해 롤백 자동화하기</title>
      <link>https://giron.tistory.com/148</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://giron.tistory.com/133&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전의 게시글&lt;/a&gt;에서 필자의 서비스에서 테스트는 @Tansactional을 통한 롤백을 제거했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 매번 테스트에서 아래의 사진처럼 DatabaseCleaner를 주입받아 반복적으로 @AfterEach로 테스트가 끝난 후, 롤백을 진행해 주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;605&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4fjcc/btrPQdcrFZU/ILTCyL3P4D4bHACjnCFpkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4fjcc/btrPQdcrFZU/ILTCyL3P4D4bHACjnCFpkK/img.png&quot; data-alt=&quot;반복했던 tearDown()&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4fjcc/btrPQdcrFZU/ILTCyL3P4D4bHACjnCFpkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4fjcc%2FbtrPQdcrFZU%2FILTCyL3P4D4bHACjnCFpkK%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;971&quot; height=&quot;605&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;605&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;반복했던 tearDown()&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 우아한테크코스 리펙토링 미션을 진행하면서 해당 방식에 대해서 알게 되었다. 이후, &lt;a href=&quot;https://junit.org/junit5/docs/5.4.0/api/org/junit/jupiter/api/extension/AfterEachCallback.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Junit5의 공식 문서&lt;/a&gt;를 확인해봤고 해당 기술에 대해서 알게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;BeforeAllCallback - @BeforeAll 적용된 메서드 전에 실행(가장 먼저 실행된다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;BeforeEachCallback - @BeforeEach 적용된 메서드전에 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;BeforeTestExecutionCallback - 각 테스트가 실행되기 직전에 실행(@Before후에 실행된다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AfterTestExecutionCallback : 각 테스트가 종료 후 실행(@AfterEach전에 실행된다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AfterEachCallback : @AfterEach 적용된 메서드 종료 후 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AfterAllCallback : 모든 테스트 종료 후 실행&amp;nbsp; (가장 나중에 실행된다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;BeforeAllCallback과 AfterAllCallBack은 딱 한 번만 실행된다.&amp;nbsp;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vyKwm/btrPVDApryE/uZrJtS2pHLKRZKLIGER1Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vyKwm/btrPVDApryE/uZrJtS2pHLKRZKLIGER1Hk/img.png&quot; data-alt=&quot;자동화를 해보자!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vyKwm/btrPVDApryE/uZrJtS2pHLKRZKLIGER1Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvyKwm%2FbtrPVDApryE%2FuZrJtS2pHLKRZKLIGER1Hk%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;1702&quot; height=&quot;760&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자동화를 해보자!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Junit 5의 AfterEachCallBack을 활용하여 자동으로 테스트가 끝난 후에 롤백을 시켜주는 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RollbackExtension&lt;/h3&gt;
&lt;pre id=&quot;code_1667119816016&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RollbackExtension implements AfterEachCallback {

    @Override
    public void afterEach(ExtensionContext context) {
        DatabaseCleaner databaseCleaner = SpringExtension.getApplicationContext(context).getBean(DatabaseCleaner.class);
        databaseCleaner.tableClear();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ServiceTest&lt;/h3&gt;
&lt;pre id=&quot;code_1667119829499&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RollbackExtension.class)
@SpringBootTest
public @interface ServiceTest {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZp2bl/btrPyGrqZ2b/ugSJGS2il9aY28AtqeCsj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZp2bl/btrPyGrqZ2b/ugSJGS2il9aY28AtqeCsj0/img.png&quot; data-alt=&quot;@AfterEachCallback적용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZp2bl/btrPyGrqZ2b/ugSJGS2il9aY28AtqeCsj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZp2bl%2FbtrPyGrqZ2b%2FugSJGS2il9aY28AtqeCsj0%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;264&quot; height=&quot;147&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;147&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@AfterEachCallback적용&lt;/figcaption&gt;
&lt;/figure&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;이제 매번 DatabaseCleaner를 Autowired로 선언 안 해줘도 되고 @AfterEach로databaseClear.tableClear()를 실행시키지 않아도 된다. 매번 테스트가 돌고난 후, &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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;truncate.sql로 매번 레포지토리를 추가해서 롤백하는 방식&lt;/li&gt;
&lt;li&gt;entityManager를 활용해서 자동으로 테이블의 이름을 매핑해서 truncate 시켜주는 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매 테스트마다 @AfterEach를 선언해서 롤백시켜주는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@AfterEachCallback을 통해 매 테스트마다 반복적인 작업을 제거&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://junit.org/junit5/docs/5.4.0/api/org/junit/jupiter/api/extension/AfterEachCallback.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://junit.org/junit5/docs/5.4.0/api/org/junit/jupiter/api/extension/AfterEachCallback.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/junit-5-extensions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/junit-5-extensions&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>JUnit5</category>
      <category>롤백</category>
      <category>테스트 자동화</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/148</guid>
      <comments>https://giron.tistory.com/148#entry148comment</comments>
      <pubDate>Sun, 30 Oct 2022 18:09:55 +0900</pubDate>
    </item>
    <item>
      <title>JPA(ORM)의 영속성컨텍스트에서 더티체킹이 좋은걸까? Lock을 통해 해결해보자</title>
      <link>https://giron.tistory.com/147</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 innodb에서 lock이 어떻게 동작하는지 어느 정도 알고 있는 상태에서 읽으면 좋을 것 같습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목에 JPA가 들어가서 주제가 JPA에 한정적으로 보일수 있겠다. 하지만 실제 해당 이슈는 JPA 뿐만 아니라 모든 곳에서 발생이 가능하다.&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;하지만 JPA에서는 dirty checking이라는 기능을 지원해주므로 더욱 조심해야 한다고 생각해서 제목으로 지어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본론으로 들어가자면 해당 상황이 위험한 이유는 lock과 관련이 있다. 그중에서 Lost Update 문제이다. (저희 서비스는 JPA와 MVCC를 지원하는 Mysql Innodb를 사용합니다.)&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;QA를 진행하다 다른 팀에서 데드락이슈를 맞이했다. 우리 팀은 괜찮을까 테스트해보다가 다른 이슈를 맞이했다. 바로 동시성 이슈였는데 해당 부분을 트러블슈팅한 기록을 남긴다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h1&gt;LOST Update 문제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자의 서비스는 JPA와 MySql을 사용한다. 따라서 해당 문제가 발생했는데 이해가 쉽도록 아래 그림을 보면서 설명하겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;1377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjuA3V/btrPfYzB0LN/TDmSbR4DIBKppiWe8dHDrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjuA3V/btrPfYzB0LN/TDmSbR4DIBKppiWe8dHDrk/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjuA3V/btrPfYzB0LN/TDmSbR4DIBKppiWe8dHDrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjuA3V%2FbtrPfYzB0LN%2FTDmSbR4DIBKppiWe8dHDrk%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;1234&quot; height=&quot;1377&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;1377&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 Giron이 SELECT할 때는 lock이 걸리지 않는 상황이다. 따라서 다른 트랜잭션인 Crew도 마찬가지로 SELECT가 가능하다. 그렇다면 각 트랜잭션에선 SELECT한 객체를 가지고 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Giron이 UPDATE 쿼리를 날리면 views가 6으로 수정이 된다. 이때는 Lock이 걸려서 Crew는 수정할 수 없다. 그리고 Giron이 COMMIT을 하게 되면서 Lock이 풀리고 Crew가 UPDATE를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Lost Update가 발생한다. 바로 업데이트 이전의 객체를 가진 Crew가 UPDATE를 했으므로 views는 다시 6이 된다. 이후 COMMIT이 된다면 views는 6이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 2번 게시글을 조회했으므로 조회수는 5&amp;rarr;7이 될것을 기대했지만 6이 된 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트로 확인해보자!&lt;/h3&gt;
&lt;pre id=&quot;code_1666427988983&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    void 게시물_조회수_증가_동시성_테스트() throws InterruptedException {
        ArticleRequest articleRequest = new ArticleRequest(&quot;질문합니다.&quot;, &quot;내용입니다~!&quot;, Category.QUESTION.getValue(),
                List.of(&quot;Spring&quot;), false);
        ArticleIdResponse savedArticle = articleService.save(new LoginMember(member.getId()), articleRequest);

        final var numberOfThreads = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        CountDownLatch latch = new CountDownLatch(numberOfThreads);

        executorService.execute(() -&amp;gt; {
            articleService.getOne(new GuestMember(), savedArticle.getId());
            latch.countDown();
        });
        executorService.execute(() -&amp;gt; {
            articleService.getOne(new GuestMember(), savedArticle.getId());
            latch.countDown();
        });

        Thread.sleep(1000);

        final var article = articleRepository.findById(savedArticle.getId())
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;Not Found&quot;));
        //정상 경우 2번 조회했으므로 2가 나와야한다. 하지만 1이 나온다.
        assertThat(article.getViews()).isEqualTo(2);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkPjE0/btrPkqaa4D6/rLWj4Zs2MCcDnrBXFapZZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkPjE0/btrPkqaa4D6/rLWj4Zs2MCcDnrBXFapZZK/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkPjE0/btrPkqaa4D6/rLWj4Zs2MCcDnrBXFapZZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkPjE0%2FbtrPkqaa4D6%2FrLWj4Zs2MCcDnrBXFapZZK%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;614&quot; height=&quot;175&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;175&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1666428033652&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: 
    update
        article 
    set
        updated_at=?,
        category=?,
        content=?,
        is_anonymous=?,
        member_id=?,
        title=?,
        views=? 
    where
        id=?
Hibernate: 
    update
        article 
    set
        updated_at=?,
        category=?,
        content=?,
        is_anonymous=?,
        member_id=?,
        title=?,
        views=? 
    where
        id=?
    &amp;gt; binding parameter [1] as [TIMESTAMP] - [2022-10-16T15:26:34.941859400]
    &amp;gt; binding parameter [2] as [VARCHAR] - [QUESTION]
    &amp;gt; binding parameter [3] as [VARCHAR] - [내용입니다~!]
    &amp;gt; binding parameter [4] as [BOOLEAN] - [false]
    &amp;gt; binding parameter [5] as [BIGINT] - [1]
    &amp;gt; binding parameter [6] as [VARCHAR] - [질문합니다.]
    &amp;gt; binding parameter [7] as [INTEGER] - [1]
    &amp;gt; binding parameter [8] as [BIGINT] - [1]
    &amp;gt; binding parameter [1] as [TIMESTAMP] - [2022-10-16T15:26:34.940859800]
    &amp;gt; binding parameter [2] as [VARCHAR] - [QUESTION]
    &amp;gt; binding parameter [3] as [VARCHAR] - [내용입니다~!]
    &amp;gt; binding parameter [4] as [BOOLEAN] - [false]
    &amp;gt; binding parameter [5] as [BIGINT] - [1]
    &amp;gt; binding parameter [6] as [VARCHAR] - [질문합니다.]
    &amp;gt; binding parameter [7] as [INTEGER] - [1]
    &amp;gt; binding parameter [8] as [BIGINT] - [1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 비동기로 돌면서 update가 1 만 된 경우를 볼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적 락
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성 이슈가 잦을 경우 사용하는 게 성능상 더 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;낙관적 락
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성 이슈가 발생할 경우가 적을 경우 사용하는게 성능상 좋다.&lt;/li&gt;
&lt;li&gt;커밋 시점에 알고 롤백이 진행되므로 동시성이 이슈가 많은 경우에 사용하면 오히려 성능이 안 좋아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비관적 락 X-Lock&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfscdo/btrPh8A56F3/mnnV8qVyDLbe3P6hbXUAZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfscdo/btrPh8A56F3/mnnV8qVyDLbe3P6hbXUAZ0/img.png&quot; data-alt=&quot;비관적 락&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfscdo/btrPh8A56F3/mnnV8qVyDLbe3P6hbXUAZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfscdo%2FbtrPh8A56F3%2FmnnV8qVyDLbe3P6hbXUAZ0%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;644&quot; height=&quot;206&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비관적 락&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락을 다음과 같이 적용하면 해결이 된다. 하지만 해당 로직이 수행하는 동안 다른 트랜잭션은 대기해야 하므로 성능상 좋지 않다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dy2NL/btrPgfVntTD/lpcKVPcfuIVdxtIKFTn7bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dy2NL/btrPgfVntTD/lpcKVPcfuIVdxtIKFTn7bk/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dy2NL/btrPgfVntTD/lpcKVPcfuIVdxtIKFTn7bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDy2NL%2FbtrPgfVntTD%2FlpcKVPcfuIVdxtIKFTn7bk%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;811&quot; height=&quot;158&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비관적 락 - S-Lock&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (4).png&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sCWCX/btrPf313lpa/yJqA9MQdsjtGNqK3Eole6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sCWCX/btrPf313lpa/yJqA9MQdsjtGNqK3Eole6k/img.png&quot; data-alt=&quot;share mod로 쿼리가 나간다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sCWCX/btrPf313lpa/yJqA9MQdsjtGNqK3Eole6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsCWCX%2FbtrPf313lpa%2FyJqA9MQdsjtGNqK3Eole6k%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;464&quot; height=&quot;563&quot; data-filename=&quot;Untitled (4).png&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;share mod로 쿼리가 나간다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;share mod로 쿼리가 나간 것을 확인할 수 있다. 하지만 데드락이 발생한다. 해당 원인은 s-lock과 x-lock사이의 관계를 이해하면 알 수 있다. 오늘의 주제가 아니므로 설명은 넘어가겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략히 설명하지만 s-lock이 걸린 상황에서 x-lock이 발생해서 서로 lock이 풀릴 때까지 기다려서 발생한 데드락이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (5).png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8gbAy/btrPgL0UBr2/9zaDwsb7cqF7mBo8HPjzL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8gbAy/btrPgL0UBr2/9zaDwsb7cqF7mBo8HPjzL0/img.png&quot; data-alt=&quot;데드락 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8gbAy/btrPgL0UBr2/9zaDwsb7cqF7mBo8HPjzL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8gbAy%2FbtrPgL0UBr2%2F9zaDwsb7cqF7mBo8HPjzL0%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;1314&quot; height=&quot;290&quot; data-filename=&quot;Untitled (5).png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데드락 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;낙관적 락&lt;/h2&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;//Article.java
@Version
private Long version;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;article도메인에 Version을 추가해줬다. 이제 UPDATE할 때, where절에 version을 같이 비교해서 만약 version이 일치하지 않는다면 OptimisticLockingFailureException 이 발생한다. 해당 예외가 발생하면 다시 요청을 하면 되는 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (7).png&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnvdrU/btrPf3OBmhw/znlXq8sCKntrl7S3OK1r5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnvdrU/btrPf3OBmhw/znlXq8sCKntrl7S3OK1r5k/img.png&quot; data-alt=&quot;ObjectOptimisticLocking이 발생했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnvdrU/btrPf3OBmhw/znlXq8sCKntrl7S3OK1r5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnvdrU%2FbtrPf3OBmhw%2FznlXq8sCKntrl7S3OK1r5k%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;1277&quot; height=&quot;297&quot; data-filename=&quot;Untitled (7).png&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ObjectOptimisticLocking이 발생했다.&lt;/figcaption&gt;
&lt;/figure&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;또한 낙관적 락을 사용하면 동시성 이슈가 발생해서 예외가 터지면 다시 시도하라는 메시지를 줄 수 있을 것 같다.&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;p data-ke-size=&quot;size16&quot;&gt;이와 같은 조회에서 동시성 문제가 생길 수 있었다. 해당 이슈가 조회여서 동시성 제어에 중요성을 못 느낄 수 있지만 아래와 같이 흔한 로직에서도 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vote exists ? update : save&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (8).png&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecGqU8/btrPjGqHP9U/szMbKrkWNCTwsaSWqK74kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecGqU8/btrPjGqHP9U/szMbKrkWNCTwsaSWqK74kk/img.png&quot; data-alt=&quot;vote exists ? update : save&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecGqU8/btrPjGqHP9U/szMbKrkWNCTwsaSWqK74kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecGqU8%2FbtrPjGqHP9U%2FszMbKrkWNCTwsaSWqK74kk%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;1071&quot; height=&quot;303&quot; data-filename=&quot;Untitled (8).png&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;vote exists ? update : save&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (9).png&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wQ8KF/btrPk97dGU3/z4z3Pjk78pKbwNIbuacp41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wQ8KF/btrPk97dGU3/z4z3Pjk78pKbwNIbuacp41/img.png&quot; data-alt=&quot;+1하고 저장을 한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wQ8KF/btrPk97dGU3/z4z3Pjk78pKbwNIbuacp41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwQ8KF%2FbtrPk97dGU3%2Fz4z3Pjk78pKbwNIbuacp41%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;749&quot; height=&quot;118&quot; data-filename=&quot;Untitled (9).png&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;+1하고 저장을 한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자의 서비스에는 해당 로직이 있다. 간략히 설명하자면 투표를 할 때, 기존의 투표가 있으면 새로운 투표 항목을 찍는 걸로 바꿔주고 &lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;이전 투표 항목의 투표수는 감소 그리고 새롭게 투표한 항목의 투표수를 증가&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;하는 로직이다. 만약 기존 투표가 없는 최초의 투표라면 해당 투표의 총개수를 1 증가하는 로직이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔한 로직인데 여기에도 마찬가지의 문제가 발생한다. 먼저 조회를 하고 insert or update를 하기 때문에 Lost update가 발생할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트로 확인해보자!&lt;/h3&gt;
&lt;pre id=&quot;code_1666428695215&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    void updateConcurrencyTest() throws InterruptedException {
        Member other = memberRepository.save(new Member(&quot;다른이&quot;, &quot;gittt&quot;, &quot;avater.url&quot;));
        LoginMember loginMember1 = new LoginMember(member.getId());
        LoginMember loginMember2 = new LoginMember(other.getId());

        final var numberOfThreads = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        CountDownLatch latch = new CountDownLatch(numberOfThreads);

        executorService.execute(() -&amp;gt; {
            voteService.doVote(discussionArticle.getId(), loginMember1,
                    new SelectVoteItemIdRequest(voteItems.get(0).getId()));
            latch.countDown();
        });
        executorService.execute(() -&amp;gt; {
            voteService.doVote(discussionArticle.getId(), loginMember2,
                    new SelectVoteItemIdRequest(voteItems.get(0).getId()));
            latch.countDown();
        });

        Thread.sleep(100);

        VoteItem voteItem = voteItemRepository.findById(1L)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;Not Found&quot;));
        //정상 경우 loginMember1, 2가 각각 투표했으므로 총 투표수는 2여야 한다.
        assertThat(voteItem.getAmount().getValue()).isEqualTo(2);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 총 투표수는 2가 되어야 하는데 1이 되었다. 조회수는 투표수에 비해 크게 안 중요하다고 할 수 있지만 내가 투표한 투표가 사라진다는 건&amp;hellip; 해결해야 할 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (10).png&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKvK3X/btrPhmsIljv/xXo98mj2MM7aOHWKrLD5F1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKvK3X/btrPhmsIljv/xXo98mj2MM7aOHWKrLD5F1/img.png&quot; data-alt=&quot;결과 실패&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKvK3X/btrPhmsIljv/xXo98mj2MM7aOHWKrLD5F1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKvK3X%2FbtrPhmsIljv%2FxXo98mj2MM7aOHWKrLD5F1%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;440&quot; height=&quot;172&quot; data-filename=&quot;Untitled (10).png&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 실패&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비관적 락 X-Lock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 비관적 락을 걸면 해결이 될 것이라고 생각을 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데드락이 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (11).png&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yK7KZ/btrPfWV6Qod/QEkVqnxu8g3hwVeK3y6gq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yK7KZ/btrPfWV6Qod/QEkVqnxu8g3hwVeK3y6gq1/img.png&quot; data-alt=&quot;데드락&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yK7KZ/btrPfWV6Qod/QEkVqnxu8g3hwVeK3y6gq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyK7KZ%2FbtrPfWV6Qod%2FQEkVqnxu8g3hwVeK3y6gq1%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;1279&quot; height=&quot;314&quot; data-filename=&quot;Untitled (11).png&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데드락&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;1&quot; data-reactroot=&quot;&quot;&gt;innodb에서는 무결성 참조 원칙으로 인해 부모 테이블에서 변경이 일어나면 외래키가 맺어진 자식 테이블의 칼럼에는 S-Lock이 걸린다.&lt;/span&gt; 따라서 아래의 로직에서 문제가 발생한 것이다. voteHistory에서 insert가 되고 foreignKey가 맺어진 voteItem에서 update가 발생하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 이슈 덕분에 투표를 기록하는 엔티티인 VoteHistory엔티티에서 외래키를 제거하는 방향으로 진행하였고 비관적 X-Lock이 정상 동작한 것을 확인할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;낙관적 Lock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Version을 명시했지만 여전히 데드락이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OptimisticLockException가 발생하기 전에 DeadLock이 발생하는 것이다. 원인은 앞선 외래키가 걸려있기 때문에 발생한 것이었다. 마찬가지로 외래키를 제거해주니 정상적으로 OptimisticLockException이 발생했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Version을 사용할 때 주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티에 @Version을 추가하고 테스트를 진행했는데 모두 깨진 상황이 발생했다. @Version 이 올라간 후부터 영속성컨텍스트에 올라가지 않아 진 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인 save메서드 내부의 isNew()메서드가 원하는 대로 작동하지 않았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;isNew()&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Id가 null이면 true가 된다.&lt;/li&gt;
&lt;li&gt;만약 @Version이 있다면 @Id 는 무시되고 Version이 null이어야 true가 된다.
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;또는 primitive타입이여도 true가 된다. id도 마찬가지이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Persistable interface를 구현한다.&lt;/b&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isNew()를 재정의하여 사용할 수 있다. 이러면 나머지 @Id 나 @Version 의 값은 무시된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Persistable interface &amp;rarr; @Version &amp;rarr; @Id 순서로 정해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 동시성에 대한 생각 없이 작성했던 코드들에 대해서 돌아보는 계기가 되었다. 또한&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 이슈를 통해서 JPA와 같은 ORM의 어두운 면을 다시금 상기할 수 있는 것 같다. 물론 ORM이 아니라고 발생하지 않는다는 보장은 없지만 단순히 더티 체킹은 편하네~ 하면서 사용하면 안 된다는 것을 깨달았다. 모든 기술에는 트레이드 오프가 있으므로 열심히 공부하고 잘 선택해야겠다.&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;Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-insert-intention-locks&quot;&gt;MySQL :: MySQL 5.7 Reference Manual :: 14.7.1 InnoDB Locking&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://vladmihalcea.com/a-beginners-guide-to-database-locking-and-the-lost-update-phenomena/&quot;&gt;A beginner's guide to database locking and the lost update phenomena - Vlad Mihalcea&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>lock</category>
      <category>s-lock</category>
      <category>x-lock</category>
      <category>공유락</category>
      <category>배타락</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/147</guid>
      <comments>https://giron.tistory.com/147#entry147comment</comments>
      <pubDate>Sat, 22 Oct 2022 18:20:13 +0900</pubDate>
    </item>
    <item>
      <title>캐시(2) 눈물나는 레디스 적용기 그런데 EmbeddedRedis가 아닌 TestConatiner를 활용한 테스트 격리와 함께</title>
      <link>https://giron.tistory.com/146</link>
      <description>&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;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;테스트가 돌아가지 않는 코드는 믿을 수가 없다! &lt;/span&gt;필자의 팀은 테스트 코드가 돌아가지 않는 코드를 ec2에 올리지 않는다. 처음 시도하는 것들이기에 테스트 코드가 보장되지 않는다면 신뢰할 수가 없기 때문이다. 따라서 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;레디스를 적용 후, 로컬에서는 정상 동작했다. 하지만 ec2의 젠킨스에서 빌드가 할 때 오류가 발생하는 문제가 발생했고 결국 레디스를 제거하였다... &amp;zwj;♂️&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqn066/btrN4lJoYBq/A3ZoEak7DutOdepHmq2MUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqn066/btrN4lJoYBq/A3ZoEak7DutOdepHmq2MUK/img.png&quot; data-alt=&quot;레디스 결국 제거...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqn066/btrN4lJoYBq/A3ZoEak7DutOdepHmq2MUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcqn066%2FbtrN4lJoYBq%2FA3ZoEak7DutOdepHmq2MUK%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;1180&quot; height=&quot;379&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;379&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레디스 결국 제거...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레디스 적용 후, 테스트하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스를 적용한 후 테스트를 진행했다. 그런데 테스트를 진행하려면 로컬에서도 레디스를 띄워야 하는 불상사가 발생한다. (test 환경과 dev환경을 분리해야 하므로) 따라서 h2처럼 내장 레디스 서버를 두어 테스트시에 아무런 조치 없이 테스트를 진행할 수 있도록 해주고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방식은 'redis test' 라는 키워드만 쳐도 볼 수 있는 baeldung이나 조졸두님의 &lt;a href=&quot;https://jojoldu.tistory.com/297&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jojoldu.tistory.com/297&lt;/a&gt; 블로그에서 참고할 수 있다. 그런데 여기서부터 불행이 시작되었다...! &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Embedded Redis&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 레디스로서 h2처럼 테스트시에만 내장으로 도는 레디스 서버라고 볼 수 있다. 여러 블로그에 있어서 잘 참고해서 적용을 했고 로컬에서도 정상적으로 동작이 되었다. 따라서 수월하게 pr을 날리고 dev에 머지를 하는 순간이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdv1U/btrN5aApVOS/LmmYK4zOmWDc4RJwKdxAz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdv1U/btrN5aApVOS/LmmYK4zOmWDc4RJwKdxAz1/img.png&quot; data-alt=&quot;깃헙 액션 정상 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdv1U/btrN5aApVOS/LmmYK4zOmWDc4RJwKdxAz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsdv1U%2FbtrN5aApVOS%2FLmmYK4zOmWDc4RJwKdxAz1%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;1522&quot; height=&quot;80&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;깃헙 액션 정상 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데...! 자꾸 젠킨스에서 빌드가 실패한다. 원인을 보면 테스트에서 실패가 일어나는데 아래와 같은 로그가 계속 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (23).png&quot; data-origin-width=&quot;1399&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ujKHQ/btrN656piuL/1vOsQ5XDG0zlwZXkzsTPO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ujKHQ/btrN656piuL/1vOsQ5XDG0zlwZXkzsTPO1/img.png&quot; data-alt=&quot;젠킨스에서 발생하는 에러&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ujKHQ/btrN656piuL/1vOsQ5XDG0zlwZXkzsTPO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FujKHQ%2FbtrN656piuL%2F1vOsQ5XDG0zlwZXkzsTPO1%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;1399&quot; height=&quot;208&quot; data-filename=&quot;Untitled (23).png&quot; data-origin-width=&quot;1399&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;젠킨스에서 발생하는 에러&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 해보면 여러 자료들이 나왔고 시도해봤지만 효과는 없었다. 내장 레디스 버전도 바꿔보고 설정도 바꿔 봤지만 여전히 그대로였다.&lt;/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에서 테스트를 돌려봤다. 같은 에러가 나오면서 실패했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 로컬에서는 돌아갔는데 ec2환경에서만 안된다는 것이 결론이었다. 그래서 환경을 살펴보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;우아한테크코스에서 지원해준 ec2는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #d44c47;&quot; data-token-index=&quot;5&quot; data-reactroot=&quot;&quot;&gt;arm아키텍처&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;64bit였습니다. 반면에 저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #337ea9;&quot; data-token-index=&quot;7&quot; data-reactroot=&quot;&quot;&gt;x86 아키텍처&lt;/span&gt;였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;혹시...?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처에 따라서 지원을 안 할 수가 있나..?라는 생각을 가지고 라이브러리를 까 보기 시작했다.  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (24).png&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMh3z3/btrN39vxc89/GlNAE9uWBZKdUsgFZWZmk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMh3z3/btrN39vxc89/GlNAE9uWBZKdUsgFZWZmk1/img.png&quot; data-alt=&quot;Embedded Redis 라이브러리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMh3z3/btrN39vxc89/GlNAE9uWBZKdUsgFZWZmk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMh3z3%2FbtrN39vxc89%2FGlNAE9uWBZKdUsgFZWZmk1%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;986&quot; height=&quot;554&quot; data-filename=&quot;Untitled (24).png&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Embedded Redis 라이브러리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보란 듯이 지원하는 아키텍처에는 x86계열 뿐이었다. 해당 원인이 EmbeddedRedis가 로컬에서는 작동하지만 ec2환경에서는 작동하지 않는 이유라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 같은 문제를 경험한 사람이 있는지 확인하기 위해 &lt;a href=&quot;https://github.com/ozimov/embedded-redis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ozimov/embedded-redis&lt;/a&gt;에 들어가서 이슈를 확인해봤다. 해당 이슈는 역시 있었고 21년 11월에 arm아키텍처에 대한 기능 추가 이슈가 있지만 아직까지 merge가 되지 않고 있었다.&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;원인을 발견하고 더이상 EmbeddedRedis를 활용해서는 테스트가 안 되겠구나.. 왜냐하면 우리의 ec2는 arm아키텍처로 돌아가기 때문에..라고 생각하고 테스트하기 위한 다른 방식을 찾아보기로 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Embedded Redis는 크게&amp;nbsp;&lt;b&gt;kstyrc 와 ozimov&lt;/b&gt;두 개의 오픈소스가 있다. 각각은 18년과 20년 이후로 업데이트가 되고 있지 않다.. arm이 아직까지 지원이 안돼 있는 거 보면 해당 방식은 이제 사용하지 않을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0nmuN/btrN40Sjioq/0mAtXDboSn13LecU9uY3eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0nmuN/btrN40Sjioq/0mAtXDboSn13LecU9uY3eK/img.png&quot; data-alt=&quot;Embedded-redis&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0nmuN/btrN40Sjioq/0mAtXDboSn13LecU9uY3eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0nmuN%2FbtrN40Sjioq%2F0mAtXDboSn13LecU9uY3eK%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;473&quot; height=&quot;84&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Embedded-redis&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/kstyrc/embedded-redis/issues/127&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/kstyrc/embedded-redis/issues/127&lt;/a&gt; 해당 pr을 보면 m1 나온 지 얼마 안 되었을 때 이야기 같은데 m1의 arm은 지원하지 않으므로 testconatiner를 사용하라고 말해준다.&lt;/p&gt;
&lt;figure id=&quot;og_1665217917104&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;can not start for mac, &amp;middot; Issue #127 &amp;middot; kstyrc/embedded-redis&quot; data-og-description=&quot;can not start for mac os version: mac os big sur 11.4 but when start in mac os catalina is ok。 is this the problem with m1 cpu or mac os ?&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/kstyrc/embedded-redis/issues/127&quot; data-og-url=&quot;https://github.com/kstyrc/embedded-redis/issues/127&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nGOcR/hyP4FKxPS2/PjXfotvTYrdjc4CHt69HF0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/kstyrc/embedded-redis/issues/127&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/kstyrc/embedded-redis/issues/127&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nGOcR/hyP4FKxPS2/PjXfotvTYrdjc4CHt69HF0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;can not start for mac, &amp;middot; Issue #127 &amp;middot; kstyrc/embedded-redis&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;can not start for mac os version: mac os big sur 11.4 but when start in mac os catalina is ok。 is this the problem with m1 cpu or mac os ?&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (25).png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWOFdb/btrN3Mt4juz/W3t2rZpUvfBuKXVWutuzek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWOFdb/btrN3Mt4juz/W3t2rZpUvfBuKXVWutuzek/img.png&quot; data-alt=&quot;모든 아키텍처에서 사용이 가능한 테스트 컨테이너를 사용하라고 한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWOFdb/btrN3Mt4juz/W3t2rZpUvfBuKXVWutuzek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWOFdb%2FbtrN3Mt4juz%2FW3t2rZpUvfBuKXVWutuzek%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;1281&quot; height=&quot;409&quot; data-filename=&quot;Untitled (25).png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모든 아키텍처에서 사용이 가능한 테스트 컨테이너를 사용하라고 한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 컨테이너&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일주일을 EmbeddedRedis를 적용하려고 날리다가 테스트 컨테이너를 적용해보자고 마음을 먹었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 레디스 테스트를 찾아보다가 테스트 컨테이너를 보긴 했고 고려도 했었다. 그런데 해당 이유로 미뤄두었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로컬에서 도커를 켜 둬야 테스트가 성공한다. 테스트 시작하고 끝날 때, 컨테이너가 뜨고 꺼지기 때문이다.&lt;/li&gt;
&lt;li&gt;속도가 느리다. 테스트할 때마다 컨테이너를 띄워야 하기 때문에 속도가 느리다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 어느 로컬에서든 격리된 환경에서 동작할 수 있다는 장점은 있었다. (EmbeddedRedis같은 경우는 로컬마다 메모리 크기가 다르면 maxMemoery를 설정해줘야 할 때도 있었다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 ec2의 arm아키텍처에서도 돌리기 위해서(젠킨스에서 돌리기 위해서) 테스트 컨테이너를 활용하기로 결정했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용법은 간단하다. 로컬에서 도커만 띄워주고 있으면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle&lt;/h4&gt;
&lt;pre id=&quot;code_1665218707016&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'

    // testcontainers
    testImplementation &quot;org.testcontainers:junit-jupiter:1.17.3&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RedisConatinerTest&lt;/h4&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;package com.woowacourse.gongseek.support;

import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
public abstract class RedisContainerTest {

    static final String REDIS_IMAGE = &quot;redis:6-alpine&quot;;
    static final GenericContainer&amp;lt;?&amp;gt; REDIS_CONTAINER;

    static {
        REDIS_CONTAINER = new GenericContainer&amp;lt;&amp;gt;(REDIS_IMAGE) // (1)
                .withExposedPorts(6379) // (2)
                .withReuse(true); // (3)
        REDIS_CONTAINER.start(); // (4)
    }

    @DynamicPropertySource // (5)
    public static void overrideProps(DynamicPropertyRegistry registry){
        registry.add(&quot;spring.redis.host&quot;, REDIS_CONTAINER::getHost);
        registry.add(&quot;spring.redis.port&quot;, () -&amp;gt; &quot;&quot;+REDIS_CONTAINER.getMappedPort(6379));
        registry.add(&quot;spring.redis.password&quot;, () -&amp;gt; &quot;password&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1): 도커에서 redis:6-alpine이미지를 pull해와서 띄운다.&lt;/li&gt;
&lt;li&gt;(2): 6379포트 번호를 노출시켜준다.&lt;/li&gt;
&lt;li&gt;(3): 컨테이너를 재사용할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;(4): container의 레디스 서버를 시작한다.&lt;/li&gt;
&lt;li&gt;(5): 동적으로 propertySource를 넣어준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;host는 &lt;span style=&quot;background-color: #ffffff; color: #383838;&quot;&gt;컨테이너 호스트인 &quot;localhost&quot;를 반환한다.&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;port는 &lt;span style=&quot;background-color: #ffffff; color: #383838;&quot;&gt;무작위로 매핑된 포트는 컨테이너 시작 후 발생하므로 해당 메소드로 런타임 시 실제 포트를 검색할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #383838;&quot;&gt;password는 임의의 password를 넣어줬다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #383838;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #383838;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;이후, 테스트를 돌려보면 아래처럼 정상적으로 컨테이너가 뜨는 것을 확인할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d06DMK/btrN5wiZCUC/b00L6gR5TVx6uqqkaLzyk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d06DMK/btrN5wiZCUC/b00L6gR5TVx6uqqkaLzyk0/img.png&quot; data-alt=&quot;컨테이너가 자동으로 실행이 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d06DMK/btrN5wiZCUC/b00L6gR5TVx6uqqkaLzyk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd06DMK%2FbtrN5wiZCUC%2Fb00L6gR5TVx6uqqkaLzyk0%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;1192&quot; height=&quot;224&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컨테이너가 자동으로 실행이 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EC2에서 도커 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 로컬에서는 컨테이너가 정상 동작했으니 젠킨스의 ec2에 도커를 설치해줘야 한다. 도커가 있어야 컨테이너를 띄우기 때문이다. ec2에 도커 설치는 도커 공식문서를 참고했는데 문서화가 정말 잘되어있다고 느꼈다. 공식문서를 따라가면 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/#set-up-the-repository&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.docker.com/engine/install/ubuntu/#set-up-the-repository&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1665220253231&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;Install Docker Engine on Ubuntu&quot; data-og-description=&quot; &quot; data-og-host=&quot;docs.docker.com&quot; data-og-source-url=&quot;https://docs.docker.com/engine/install/ubuntu/#set-up-the-repository&quot; data-og-url=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tot5p/hyP3vQkHbV/2wIQKykgnKsVSQnSpYe720/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/rqBUR/hyP3viv1vJ/nA3BU71yRBpgp4nOF50NPK/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/#set-up-the-repository&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.docker.com/engine/install/ubuntu/#set-up-the-repository&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tot5p/hyP3vQkHbV/2wIQKykgnKsVSQnSpYe720/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/rqBUR/hyP3viv1vJ/nA3BU71yRBpgp4nOF50NPK/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Install Docker Engine on Ubuntu&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문서를 참고해서 설치를 해주면 완료가 된다. 그런데 도커의 사용자에 ubuntu사용자가 없으므로 매번 sudo를 사용해야 한다. 따라서 설치만 한 상태에서 테스트를 돌리면 실행 권한이 없어서 컨테이너가 뜨지 않아 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 도커 실행 권한을 줘야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도커 사용자 추가하기&lt;/h4&gt;
&lt;pre id=&quot;code_1665221948924&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo usermod -aG docker $USER // 도커 그룹에 사용자 추가하기
$ sudo chmod 660 /var/run/docker.sock // 만약 위의 방식으로 사용자 추가해도 안된다면 이 명령어로 다른 사용자도 접근하게 해주자 
$ sudo service docker restart&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chmod에 조금 더 알고싶다면 &amp;gt;&amp;gt; &lt;a href=&quot;https://giron.tistory.com/116&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://giron.tistory.com/116&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1665221923213&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;리눅스 명령어와 권한 설정 chmod&quot; data-og-description=&quot;uniq 중복된 문자열을 제거한다. 이때 정렬된 문자열에서 중복되어야만 제거가 된다. 즉, sort | uniq는 거의 세트라고 생각해도 될 것 같다. history 지금까지 사용했던 명령어들을 보여준다. ex) history&quot; data-og-host=&quot;giron.tistory.com&quot; data-og-source-url=&quot;https://giron.tistory.com/116&quot; data-og-url=&quot;https://giron.tistory.com/116&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2NXvn/hyP3ujBDGo/TAjYhZ3NZRujO1fhnKINwK/img.png?width=440&amp;amp;height=34&amp;amp;face=0_0_440_34,https://scrap.kakaocdn.net/dn/nQOCS/hyP3Axk9I8/MdR9cVtsTmqx8AiJawQchK/img.png?width=440&amp;amp;height=34&amp;amp;face=0_0_440_34,https://scrap.kakaocdn.net/dn/buSBP5/hyP4J7ieSL/r4iEkrSbFAyy5BTnQK6emK/img.jpg?width=1080&amp;amp;height=810&amp;amp;face=0_0_1080_810&quot;&gt;&lt;a href=&quot;https://giron.tistory.com/116&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://giron.tistory.com/116&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2NXvn/hyP3ujBDGo/TAjYhZ3NZRujO1fhnKINwK/img.png?width=440&amp;amp;height=34&amp;amp;face=0_0_440_34,https://scrap.kakaocdn.net/dn/nQOCS/hyP3Axk9I8/MdR9cVtsTmqx8AiJawQchK/img.png?width=440&amp;amp;height=34&amp;amp;face=0_0_440_34,https://scrap.kakaocdn.net/dn/buSBP5/hyP4J7ieSL/r4iEkrSbFAyy5BTnQK6emK/img.jpg?width=1080&amp;amp;height=810&amp;amp;face=0_0_1080_810');&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;리눅스 명령어와 권한 설정 chmod&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;uniq 중복된 문자열을 제거한다. 이때 정렬된 문자열에서 중복되어야만 제거가 된다. 즉, sort | uniq는 거의 세트라고 생각해도 될 것 같다. history 지금까지 사용했던 명령어들을 보여준다. ex) history&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;giron.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 글을 쓰는 시점이어서 9일전이지만 실제는 레디스 테스트에 일주일 정도 걸린 것 같다. 레디스를 처음 적용해보면서 여러 시행착오가 있었는데 많은 경험을 할 수 있어서 힘들었지만 좋은 경험이라고 생각한다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1529&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSzaki/btrN4DwkDyH/ijRivbv6hKmTtSfQLwqrUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSzaki/btrN4DwkDyH/ijRivbv6hKmTtSfQLwqrUk/img.png&quot; data-alt=&quot;처음 적용했을 때,&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSzaki/btrN4DwkDyH/ijRivbv6hKmTtSfQLwqrUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSzaki%2FbtrN4DwkDyH%2FijRivbv6hKmTtSfQLwqrUk%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;1529&quot; height=&quot;80&quot; data-origin-width=&quot;1529&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;처음 적용했을 때,&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 아래처럼 정상적으로 테스트가 CI환경에서도 작동하는 것을 볼 수 있다. 이렇게 해서&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;redis에서도 테스트 격리에 성공을 했다. 다만 이전 게시글에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;테스트 코드&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;최적화로 속도를 줄였지만 컨테이너를 띄우면서 속도가 다시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;느려진 건&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;어쩔 수 없는&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;부분인가 보다.. &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;1521&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MFL9E/btrN6mmSZtp/3fRrRvSVjcpXu7koxyMjM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MFL9E/btrN6mmSZtp/3fRrRvSVjcpXu7koxyMjM1/img.png&quot; data-alt=&quot;ci성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MFL9E/btrN6mmSZtp/3fRrRvSVjcpXu7koxyMjM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMFL9E%2FbtrN6mmSZtp%2F3fRrRvSVjcpXu7koxyMjM1%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;1521&quot; height=&quot;83&quot; data-origin-width=&quot;1521&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ci성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://loosie.tistory.com/813&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://loosie.tistory.com/813&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>Docker</category>
      <category>REDIS</category>
      <category>TestContainer</category>
      <category>테스트</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/146</guid>
      <comments>https://giron.tistory.com/146#entry146comment</comments>
      <pubDate>Tue, 11 Oct 2022 22:42:01 +0900</pubDate>
    </item>
    <item>
      <title>캐시(1) Redis란?</title>
      <link>https://giron.tistory.com/145</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1247&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mFId1/btrMmgB8khF/KUSQfZ9ZIknGyJY0GAB9SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mFId1/btrMmgB8khF/KUSQfZ9ZIknGyJY0GAB9SK/img.png&quot; data-alt=&quot;레디스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mFId1/btrMmgB8khF/KUSQfZ9ZIknGyJY0GAB9SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmFId1%2FbtrMmgB8khF%2FKUSQfZ9ZIknGyJY0GAB9SK%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;1247&quot; height=&quot;597&quot; data-origin-width=&quot;1247&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레디스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis(Remote Dictionary Server)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;key-value&amp;rdquo; 구조로 데이터를 저장하고 관리하기 위한 &lt;b&gt;비 관계형 데이터베이스&lt;/b&gt;로 모든 데이터를 메모리에서 처리하는 메모리 기반 DB이다. 메모리를 통해 사용하기 때문에 일반적인 DB보다 빠른 성능을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스는 싱글 스레드로 동작한다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 디비에 접근해서 디스크를 일고 쓰는 것은 비용이 크다. 따라서 데이터를 메모리에 두어 디스크에 접근을 최소화하는 방법이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 정보를 캐시할까&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘 안 바뀌는데, 자주 읽어오는 경우 ex) 로그인 정보&lt;/li&gt;
&lt;li&gt;key마다 태그를 달아서 어떤 데이터가 많이 생성되는지 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐시 vs 버퍼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼는 주로 차기를 기다려서 꽉 차면 보내주고, cache는 미리 계산하고 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼는 쓰기를 위해서 주로 저장한다. 느린 것을 덜 느리게 사용하기 위해서 그리고 캐시는 읽기에서 더 빠른 성능을 위해서이다. 빠른 것을 더 빠르게 하기 위해서&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 캐시(EHCACHE) vs In memory DB&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;속도는 당연히 로컬 캐시가 빠르다. ex) Integer 125, 캐시가 없어지면 안 되기 때문에 또 문제다.&lt;/li&gt;
&lt;li&gt;동기화에 문제가 있다. 단순히 서버가 2개로만 늘어나도 하나의 서버에서 데이터가 바뀌면 다른 서버에 통보해줘야 한다. 이때, 동기화의 문제가 발생할 수 있다. 맞추기도 어렵고 똑같은 정보를 모든 서버가 같이 들고 있기 때문에 중복이 발생한다.
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서 외부에 인메모리 디비를 두면 동기화에 더 유연하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터베이스에 저장하면 될 거 같은데 인메모리 디비는 왜 사용할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크는 초당 천 번 정도 쓰면 메모리에 접근하면 10만 번 정도 가능하다. 디스크는 물리적으로 팬이 헤드가 돌아야 하므로 느리다. 디스크를 쓰는 이유는 영속적으로 저장하기 위해서이다. 대신 메모리에 올리므로 날아갈 수 있다. 따라서 날아가도 괜찮은 것을 저장해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날아가면 안 되는 것 - store&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날아가도 되는 것 - cache&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐시가 다운될 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 캐시가 장애가 나면 모든 트래픽이 디비로 가므로 디비는 항상 캐시를 염두에 두고 디비 크기를 구성해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멀티 플렉싱&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 동작 원리를 살펴보면 Redis는 이벤트 루프(Event Loop)와 비슷한 multiplexing을 이용하여 요청을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실제 명령에 대한 작업(Task)은 커널 레벨에서 멀티플렉싱(Multiplexing)을 통해 처리하여 동시성을 보장합니다. 쉽게 유저 레벨에서는 싱글 스레드로 동작하지만, 커널 I/O 레벨에서는 4개의 스레드 풀을 이용하는 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐시 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Write Through 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시와 DB에 항상 동시에 데이터를 기록하는 방식입니다. 항상 최신의 데이터가 유지되지만, 쓰기 작업이 많을 경우 캐시와 DB에 매번 통신하는 비용이 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Write Back 구조&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1163&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daj7eP/btrMvsJ2LQL/F8RsaYCShzgK0Q5e6ha1i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daj7eP/btrMvsJ2LQL/F8RsaYCShzgK0Q5e6ha1i0/img.png&quot; data-alt=&quot;write back&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daj7eP/btrMvsJ2LQL/F8RsaYCShzgK0Q5e6ha1i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdaj7eP%2FbtrMvsJ2LQL%2FF8RsaYCShzgK0Q5e6ha1i0%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;1163&quot; height=&quot;452&quot; data-origin-width=&quot;1163&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;write back&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 캐시에만 기록합니다. 이후에 캐시에 있는 데이터를 DB에 한번에 등록합니다.장점은 쓰기가 많이 일어나도 캐시에만 업데이트 되기 때문에 빠른 처리가 가능합니다. 하지만 캐시 자체는 휘발성 메모리라서 데이터 유실 가능성이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Look Aside 구조&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F34pY/btrMxC6rdbc/qC8ywYxjQzynaH5JzYxKek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F34pY/btrMxC6rdbc/qC8ywYxjQzynaH5JzYxKek/img.png&quot; data-alt=&quot;Look aside Cache&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F34pY/btrMxC6rdbc/qC8ywYxjQzynaH5JzYxKek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF34pY%2FbtrMxC6rdbc%2FqC8ywYxjQzynaH5JzYxKek%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;1205&quot; height=&quot;475&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Look aside Cache&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 캐시에 데이터가 있는 cache hit 시에는 캐시에서 데이터를 가져오고, 캐시에 데이터가 없는 cache miss 시에는 DB에서 데이터를 가져와 다시 캐시에 저장하고 결과를 반환하는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cache miss가 일어나도, DB에서 다시 가져올 수 있다. 하지만 캐시에 데이터가 있는 채로 DB 데이터가 변경되었다면, 캐시와 DB 데이터가 다른 불일치 문제가 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TTL존재 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 Look aside 구조를 적용한다면 데이터 불일치 문제에 대한 해결 전략을 추가해야합니다. 스프링 데이터 레디스 문서에서는 TTL(Time to Live)이라는 기능을 제공합니다. redis 데이터의 만료기간을 설정하여, TTL기간이 지나면 캐시 데이터가 없어지게 됩니다. TTL을 설정함으로써 자연스럽게 캐시 값이 없어지고, 자가진단 관련 데이터 조회 시 DB 데이터를 캐시에 저장하게되면서 최신의 DB 데이터를 캐시에 담을 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 문제 해결을 위해 Look Aside 구조와 TTL을 적용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 vs 병렬성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성: 하나의 코어에서 2개의 쓰레드가 번갈아가면서 작업할 때&lt;/li&gt;
&lt;li&gt;병렬성: 아예 다른 2개의 코어가 각자 일을 할 때,&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글 스레드인 이유는?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Context-Switch(문맥교환)가 없어 성능이 더 좋다.&lt;/li&gt;
&lt;li&gt;교착 상태(데드락)을 고려하지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EXPIRE 처리 방법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수동 처리: key를 확인할 때, 만료시간을 확인해서 없앤다.&lt;/li&gt;
&lt;li&gt;자동 처리(default): 만료가 있는 키 20개 정도를 확인한다. &amp;rarr; 만료 키를 제거한다 &amp;rarr; 25% 정도 제거된다면 다시 이 사이클을 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis vs Memcached&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;공통점&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;key-value 자료구조를 가진다.&lt;/li&gt;
&lt;li&gt;만료일을 지정해서 만료일이 지난 데이터를 삭제할 수 있다. (하지만 지원 방법은 다르다)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MemCached의 경우, LRU(Least Recently Used) 알고리즘만 채택한다.&lt;/li&gt;
&lt;li&gt;Redis는 다양한 알고리즘 채택할 수 있다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;No Eviction: 기한을 정하지 않는다. 메모리가 부족하면 에러를 발생한다.&lt;/li&gt;
&lt;li&gt;All Keys LRU: LRU에 근거해서 삭제한다.&lt;/li&gt;
&lt;li&gt;Volatile LRU: LRU에 근거하되 &lt;b&gt;만료 시점이 지정된&lt;/b&gt; 것들에 한해서 삭제한다.&lt;/li&gt;
&lt;li&gt;All Keys Random: 랜덤하게 키 삭제한다.&lt;/li&gt;
&lt;li&gt;Volatile Random: 랜덤하게 키 삭제하되&amp;nbsp;&lt;b&gt;만료 시점이 지정된&lt;/b&gt; 것들에 한해서 삭제한다.&lt;/li&gt;
&lt;li&gt;Volatile TTL: TTL 값을 기반으로, 만료 시점이 빨리 도래하는 순서대로 삭제한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;Redis&lt;/span&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;span style=&quot;letter-spacing: 0px;&quot;&gt;collection같은 data structure를 제공해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Master-Slaves 구조로 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Pub/ Sub Messag을 지원하여 채팅, 실시간 스트리밍에서 사용 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;싱글 스레드이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;트랜잭션을 지원한다.&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;MULTI: 트랜잭션 시작&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;DISCARD: 트랜잭션 취소&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;EXEC: 트랜잭션 커밋&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;WATCH: 특정 key의 변경 여부 감시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;UNWATCH: WATCH취소&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;쓰기 성능 증가를 위한 샤딩 지원&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;Memcached&lt;/span&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;span&gt;멀티스레드 구조이므로 redis보다 성능이 훨씬 빠르다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;키 벨류 꺼내는 get/set 밖에 없다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;===================================&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lettuce vs Jedis&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data Redis에서 사용할 수 있는 Redis Client 구현체는 크게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Lettuce&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Jedis&lt;/b&gt;가 있다. 결론부터 이야기하자면, 이번 프로젝트에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Lettuce를 사용&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-boot-starter-data-redis&amp;nbsp;을 사용하면 별도의 의존성 설정 없이 Lettuce를 사용할 수 있다. 반면 Jedis는 별도의 설정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jedis를 사용하지 않는 이유는 몇 가지 더 있다. 이동욱님의 아래의&amp;nbsp;&lt;b&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;포스팅을 읽어보면, Lettuce가 더 높은 성능을 내고, 문서도 더 잘 되어있고, 오픈소스도 더 잘 관리되고 있다고 한다. 이런 여러 이유로 이번 프로젝트에서는 Lettuce를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/418&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jojoldu.tistory.com/418&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1663435340402&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Jedis 보다 Lettuce 를 쓰자&quot; data-og-description=&quot;Java의 Redis Client는 크게 2가지가 있습니다. Jedis Lettuce 둘 모두 몇천개의 Star를 가질만큼 유명한 오픈소스입니다. 이번 시간에는 둘 중 어떤것을 사용해야할지에 대해 성능 테스트 결과를 공유하&quot; data-og-host=&quot;jojoldu.tistory.com&quot; data-og-source-url=&quot;https://jojoldu.tistory.com/418&quot; data-og-url=&quot;https://jojoldu.tistory.com/418&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dQIx2H/hyPQpAEy3G/c7wy5giux8JOdxgyJvrCL0/img.png?width=562&amp;amp;height=382&amp;amp;face=0_0_562_382,https://scrap.kakaocdn.net/dn/cta5fJ/hyPQreaqf9/Yk4JdP0jrhIddhb4Wxkj7k/img.png?width=562&amp;amp;height=382&amp;amp;face=0_0_562_382,https://scrap.kakaocdn.net/dn/bpyTrc/hyPOSxLNfa/ufUp34shHvMeFRDX0PBoO0/img.png?width=1984&amp;amp;height=914&amp;amp;face=0_0_1984_914&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/418&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jojoldu.tistory.com/418&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dQIx2H/hyPQpAEy3G/c7wy5giux8JOdxgyJvrCL0/img.png?width=562&amp;amp;height=382&amp;amp;face=0_0_562_382,https://scrap.kakaocdn.net/dn/cta5fJ/hyPQreaqf9/Yk4JdP0jrhIddhb4Wxkj7k/img.png?width=562&amp;amp;height=382&amp;amp;face=0_0_562_382,https://scrap.kakaocdn.net/dn/bpyTrc/hyPOSxLNfa/ufUp34shHvMeFRDX0PBoO0/img.png?width=1984&amp;amp;height=914&amp;amp;face=0_0_1984_914');&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;Jedis 보다 Lettuce 를 쓰자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Java의 Redis Client는 크게 2가지가 있습니다. Jedis Lettuce 둘 모두 몇천개의 Star를 가질만큼 유명한 오픈소스입니다. 이번 시간에는 둘 중 어떤것을 사용해야할지에 대해 성능 테스트 결과를 공유하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jojoldu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;======&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Repository vs Redis Template&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트에서 Redis를 사용하는 방법에는 두 가지가 있다. Repository 인터페이스를 정의하는 방법과 Redis Template을 사용하는 방법이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Repository&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDdfqw/btrMtFwdsM2/aNrkL0NlUydFuMB1DsCil0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDdfqw/btrMtFwdsM2/aNrkL0NlUydFuMB1DsCil0/img.png&quot; data-alt=&quot;공식문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDdfqw/btrMtFwdsM2/aNrkL0NlUydFuMB1DsCil0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDdfqw%2FbtrMtFwdsM2%2FaNrkL0NlUydFuMB1DsCil0%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;967&quot; height=&quot;186&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공식문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 인터페이스를 정의하는 방법은 Spring Data JPA를 사용하는 것과 비슷하다. Redis는 많은 자료구조를 지원하는데, Repository를 정의하는 방법은 Hash 자료구조로 한정하여 사용할 수 있다. Repository를 사용하면 객체를 Redis의 Hash 자료구조로 직렬화하여 스토리지에 저장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 트랜잭션을 지원하지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis Template&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Template은 Redis 서버에 커맨드를 수행하기 위한 고수준의 추상화(high-level abstraction)를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transactional을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드명반환 오퍼레이션관련 Redis 자료구조&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;opsForValue()&lt;/td&gt;
&lt;td&gt;ValueOperations&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opsForList()&lt;/td&gt;
&lt;td&gt;ListOperations&lt;/td&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opsForSet()&lt;/td&gt;
&lt;td&gt;ListOperations&lt;/td&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opsForList()&lt;/td&gt;
&lt;td&gt;SetOperations&lt;/td&gt;
&lt;td&gt;Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opsForZSet()&lt;/td&gt;
&lt;td&gt;ZSetOperations&lt;/td&gt;
&lt;td&gt;Sorted Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opsForHash()&lt;/td&gt;
&lt;td&gt;HashOperations&lt;/td&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 도입한 이유는 저희 서비스의 인증 방식이 액세스 토큰과 리프레시 토큰을 사용한다. 이때, 액세스 토큰이 만료될 때마다 디비에서 리프레시 토큰을 비교하여 유효하면 재발급해주는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 이처럼 잦은 요청과 응답이 발생하므로 인 메모리 디비인 레디스를 사용하여 성능을 높이기 위해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. redis는 만료 기간이 지나면 자동으로 삭제를 해주는 옵션을 줄 수 있다. 따라서 현재 서비스에서 이전에 발급했던 토큰을 디비에 저장해서 탈취당했는지의 여부를 판단한다. 일정 기간이 지나면 토큰이 무효화되기 때문에 디비에서 지워야 하는데 레디스를 사용하지 않으면 스케쥴러를 돌려야 한다. 즉, 비즈니스 로직에만 집중할 수 있지 못한다. 불필요한 코드를 만든다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https://www.youtube.com/watch?v=mPB2CZiAkKM&amp;amp;t=3978s&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=zkbvFOwJFgA&amp;amp;t=3225s&quot;&gt;https://www.youtube.com/watch?v=zkbvFOwJFgA&amp;amp;t=3225s&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>memcache</category>
      <category>REDIS</category>
      <category>캐시</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/145</guid>
      <comments>https://giron.tistory.com/145#entry145comment</comments>
      <pubDate>Sun, 9 Oct 2022 14:40:00 +0900</pubDate>
    </item>
    <item>
      <title>공식팀의 테스트코드 최적화 with 테스트 격리</title>
      <link>https://giron.tistory.com/144</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 저희 공식팀은 인수 테스트와 @WebMvc를 통한 컨트롤러 테스트, @SpringBootTest를 활용한 서비스 테스트, 그리고 @DataJpaRepository를 활용한 레포지토리 테스트로 크게 4종류의 테스트를 진행 중입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 격리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 격리의 방식으로는 다양한 방식이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DirtiesContext 사용하기
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 쉬운 방법이지만 매번 컨텍스트를&amp;nbsp;올려야 하므로 느린 방식이어서 저희 팀은 처음부터 해당 방식을 적용하지 않았습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;@Sql을 활용한 sql문 미리 작성하기
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 방법을 미션 하면서 적용해보았다. 그때는 규모도 작고 페어로 진행하기 때문에 크게 문제가 되지 않았다.&lt;/li&gt;
&lt;li&gt;그런데 팀 프로젝트에 적용하기엔 아래와 같은 단점이 많았다. 따라서 이것도 적용하지 못했다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트에 필요한 더미 데이터가 무엇인지 확인하려면 매번 sql파일을 봐야 해서 번거롭다.&lt;/li&gt;
&lt;li&gt;테이블이 추가되거나 칼럼이 변경되는 등 매번 sql문과의 싱크를 확인해줘야 하는 불편함이 있다&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;@Transactional을 이용한 격리
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분 사용하는 경우일 것이다. 하지만 인수 테스트에서는 HTTP요청을 보내는 client와 서버가 각각 다른 스레드에서 실행되므로 무의미하다.&lt;/li&gt;
&lt;li&gt;인수 테스트 이외의 곳에서는 다음과 같은 이유로 사용하지 않고 있다. &amp;gt;&amp;gt; &lt;a title=&quot;롤백목적으로 @Transactional사용을 지양하자&quot; href=&quot;https://giron.tistory.com/133&quot;&gt;https://giron.tistory.com/133&lt;/a&gt;/&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;truncate를 활용한 DataBaseCleaner설정
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional 사용하지 못해서 결정한 방법이다. 해당 방식은 자동으로 테스트가 끝날 때마다 truncate를 통해 롤백시켜준다. -&amp;gt; &lt;span&gt;현재 저희 공식팀에서 채택한 방법이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;진짜 최적화 이야기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞의 서론을 말한 이유는 이미 저희 팀은 더티컨텍스트를 사용하지 않아서 나쁘지 않은 테스트 속도를 보이고 있습니다. 노트북 사양마다 다르지만 365개 테스트 기준으로 30초 안팎을 기록하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (21).png&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKxXej/btrN3VjU6f3/HzT5BaAcXrFzvxmF76H9Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKxXej/btrN3VjU6f3/HzT5BaAcXrFzvxmF76H9Xk/img.png&quot; data-alt=&quot;363개일때 테스트 속도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKxXej/btrN3VjU6f3/HzT5BaAcXrFzvxmF76H9Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKxXej%2FbtrN3VjU6f3%2FHzT5BaAcXrFzvxmF76H9Xk%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;366&quot; height=&quot;68&quot; data-filename=&quot;Untitled (21).png&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;363개일때 테스트 속도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 체감상으로는 여전히 느리다고 느꼈습니다. 실제 젠킨스에서는 Build and Test 가 2분이 걸리는 것을 확인할 수 있습니다.&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;659&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9we40/btrN5beYrd8/SBSbeg997z8YUHpWUPCeT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9we40/btrN5beYrd8/SBSbeg997z8YUHpWUPCeT1/img.png&quot; data-alt=&quot;젠킨스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9we40/btrN5beYrd8/SBSbeg997z8YUHpWUPCeT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9we40%2FbtrN5beYrd8%2FSBSbeg997z8YUHpWUPCeT1%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;659&quot; height=&quot;109&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;젠킨스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더티 컨텍스트는 이미 제거되었고 다른 테스트 속도를 줄이는 방법이 무엇이 있을까 고민을 했고 찾은 방법으로는 컨텍스트 캐싱을 활용하자는 결론이 나왔습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-rnw-media-class=&quot;2308-196__2306-_b1430-196&quot;&gt;
&lt;div&gt;
&lt;div data-rnw-media-class=&quot;2307-__2305&quot;&gt;
&lt;div&gt;
&lt;div data-key=&quot;656a4ee250ea4c9f95a203482cea919f&quot;&gt;
&lt;div data-block-content=&quot;656a4ee250ea4c9f95a203482cea919f&quot;&gt;
&lt;h3 id=&quot;text-6.1&quot; data-rnw-media-class=&quot;211__210&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span data-key=&quot;a9405f9b5be34ab4a6c160b318f901d1&quot;&gt;&lt;span data-offset-key=&quot;a9405f9b5be34ab4a6c160b318f901d1:0&quot;&gt;테스트 컨텍스트 프레임워크&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-slate-fragment=&quot;JTdCJTIyb2JqZWN0JTIyJTNBJTIyZG9jdW1lbnQlMjIlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJoZWFkaW5nLTElMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMiVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFQyVCQiVBOCVFRCU4NSU4RCVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFRCU5NCU4NCVFQiVBMCU4OCVFQyU5RSU4NCVFQyU5QiU4QyVFRCU4MSVBQyUyMiUyQyUyMm1hcmtzJTIyJTNBJTVCJTVEJTJDJTIyc2VsZWN0aW9ucyUyMiUzQSU1QiU1RCU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMmQzMmQwNTQ4MzZiYzRlMTZiNGRlMDEzMzlhNzcxZmU0JTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyNWJiYjEzNzQ4YTk2NDNiMmI1NGQ2YmMwMGZmNzRjYjglMjIlN0QlMkMlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJsaXN0LXVub3JkZXJlZCUyMiUyQyUyMmlzVm9pZCUyMiUzQWZhbHNlJTJDJTIyZGF0YSUyMiUzQSU3QiU3RCUyQyUyMm5vZGVzJTIyJTNBJTVCJTdCJTIyb2JqZWN0JTIyJTNBJTIyYmxvY2slMjIlMkMlMjJ0eXBlJTIyJTNBJTIybGlzdC1pdGVtJTIyJTJDJTIyaXNWb2lkJTIyJTNBZmFsc2UlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJwYXJhZ3JhcGglMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMiVFQyU4QSVBNCVFRCU5NCU4NCVFQiVBNyU4MSVFQyU5RCU4MCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyU4MiVBQyVFQyU5QSVBOSVFQiU5MCU5OCVFQiU4QSU5NCUyMCVFQyU5NSVBMCVFRCU5NCU4QyVFQiVBNiVBQyVFQyVCQyU4MCVFQyU5RCVCNCVFQyU4NSU5OCUyMCVFQyVCQiVBOCVFRCU4NSU4RCVFQyU4QSVBNCVFRCU4QSVCOCVFQiVBNSVCQyUyMCVFQyU4MyU5RCVFQyU4NCVCMSVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFQSVCNCU4MCVFQiVBNiVBQyVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyVBMCU4MSVFQyU5QSVBOSVFRCU5NSVCNCVFQyVBMyVCQyVFQiU4QSU5NCUyMCVFQSVCOCVCMCVFQiU4QSVBNSVFQyU5RCU4NCUyMCVFQSVCMCU4MCVFQyVBNyU4NCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFRCU5NCU4NCVFQiVBMCU4OCVFQyU5RSU4NCVFQyU5QiU4QyVFRCU4MSVBQyVFQiVBNSVCQyUyMCVFQyVBMCU5QyVFQSVCMyVCNSVFRCU5NSU5QyVFQiU4QiVBNC4lMjAlRUMlOUQlQjQlRUIlQTUlQkMlMjAlRUQlODUlOEMlRUMlOEElQTQlRUQlOEElQjglMjAlRUMlQkIlQTglRUQlODUlOEQlRUMlOEElQTQlRUQlOEElQjglMjAlRUQlOTQlODQlRUIlQTAlODglRUMlOUUlODQlRUMlOUIlOEMlRUQlODElQUMlRUIlOUQlQkMlRUElQjMlQTAlMjAlRUIlQjYlODAlRUIlQTUlQjglRUIlOEIlQTQlMjIlMkMlMjJtYXJrcyUyMiUzQSU1QiU1RCUyQyUyMnNlbGVjdGlvbnMlMjIlM0ElNUIlNUQlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjI2MjY2ZjQxYjlhMDI0N2IzYWMzZTYwYzEyMTI2NTU3ZSUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjk0Y2I5MTIyMDQyNjRjNGRiNTAyOGYyZTc4YTc3YWM4JTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYTY4NDc5ZGEzNDIwNDA0MDk0OTQ4NDlkYzI1OWRjMDElMjIlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjIzYzUwZWYxYWFkNTA0MjNiYmRiOGZhYmE5NDMwYTM1OCUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjQ3YmE4NGZmNjM0MDRlZDk5ZDI5MmU0NGE3MTQwZDc0JTIyJTdE&quot;&gt;&lt;span data-slate-fragment=&quot;JTdCJTIyb2JqZWN0JTIyJTNBJTIyZG9jdW1lbnQlMjIlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJwYXJhZ3JhcGglMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMiVFQyU4QSVBNCVFRCU5NCU4NCVFQiVBNyU4MSVFQyU5RCU4MCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyU4MiVBQyVFQyU5QSVBOSVFQiU5MCU5OCVFQiU4QSU5NCUyMCVFQyU5NSVBMCVFRCU5NCU4QyVFQiVBNiVBQyVFQyVCQyU4MCVFQyU5RCVCNCVFQyU4NSU5OCUyMCVFQyVCQiVBOCVFRCU4NSU4RCVFQyU4QSVBNCVFRCU4QSVCOCVFQiVBNSVCQyUyMCVFQyU4MyU5RCVFQyU4NCVCMSVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFQSVCNCU4MCVFQiVBNiVBQyVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyVBMCU4MSVFQyU5QSVBOSVFRCU5NSVCNCVFQyVBMyVCQyVFQiU4QSU5NCUyMCVFQSVCOCVCMCVFQiU4QSVBNSVFQyU5RCU4NCUyMCVFQSVCMCU4MCVFQyVBNyU4NCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFRCU5NCU4NCVFQiVBMCU4OCVFQyU5RSU4NCVFQyU5QiU4QyVFRCU4MSVBQyVFQiVBNSVCQyUyMCVFQyVBMCU5QyVFQSVCMyVCNSVFRCU5NSU5QyVFQiU4QiVBNC4lMjAlRUMlOUQlQjQlRUIlQTUlQkMlMjAlRUQlODUlOEMlRUMlOEElQTQlRUQlOEElQjglMjAlRUMlQkIlQTglRUQlODUlOEQlRUMlOEElQTQlRUQlOEElQjglMjAlRUQlOTQlODQlRUIlQTAlODglRUMlOUUlODQlRUMlOUIlOEMlRUQlODElQUMlRUIlOUQlQkMlRUElQjMlQTAlMjAlRUIlQjYlODAlRUIlQTUlQjglRUIlOEIlQTQuJTIyJTJDJTIybWFya3MlMjIlM0ElNUIlNUQlMkMlMjJzZWxlY3Rpb25zJTIyJTNBJTVCJTVEJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYzU2MWJiNGM1NWY1NDNhNmFkZjRhNzRmMmNjMThiMGIlMjIlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjI0NjQyOGE5MTA3NmU0M2FiYjU1OGZiYzhhNzcyODI3YiUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMmJhNmIwOGVmYzdiYjRhNzQ5MDY1YWE5OTI1NjUzNDVjJTIyJTdE&quot;&gt;스프링은 테스트에 사용되는 애플리케이션 컨텍스트를 생성하고 관리하고 테스트에 적용해주는 기능을 가진 테스트 프레임워크를 제공한다. 이를 테스트 컨텍스트 프레임워크라고 부른다.&lt;/span&gt;&lt;/div&gt;
&lt;div data-slate-fragment=&quot;JTdCJTIyb2JqZWN0JTIyJTNBJTIyZG9jdW1lbnQlMjIlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJoZWFkaW5nLTElMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMiVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFQyVCQiVBOCVFRCU4NSU4RCVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFRCU5NCU4NCVFQiVBMCU4OCVFQyU5RSU4NCVFQyU5QiU4QyVFRCU4MSVBQyUyMiUyQyUyMm1hcmtzJTIyJTNBJTVCJTVEJTJDJTIyc2VsZWN0aW9ucyUyMiUzQSU1QiU1RCU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMmQzMmQwNTQ4MzZiYzRlMTZiNGRlMDEzMzlhNzcxZmU0JTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyNWJiYjEzNzQ4YTk2NDNiMmI1NGQ2YmMwMGZmNzRjYjglMjIlN0QlMkMlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJsaXN0LXVub3JkZXJlZCUyMiUyQyUyMmlzVm9pZCUyMiUzQWZhbHNlJTJDJTIyZGF0YSUyMiUzQSU3QiU3RCUyQyUyMm5vZGVzJTIyJTNBJTVCJTdCJTIyb2JqZWN0JTIyJTNBJTIyYmxvY2slMjIlMkMlMjJ0eXBlJTIyJTNBJTIybGlzdC1pdGVtJTIyJTJDJTIyaXNWb2lkJTIyJTNBZmFsc2UlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJwYXJhZ3JhcGglMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMiVFQyU4QSVBNCVFRCU5NCU4NCVFQiVBNyU4MSVFQyU5RCU4MCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyU4MiVBQyVFQyU5QSVBOSVFQiU5MCU5OCVFQiU4QSU5NCUyMCVFQyU5NSVBMCVFRCU5NCU4QyVFQiVBNiVBQyVFQyVCQyU4MCVFQyU5RCVCNCVFQyU4NSU5OCUyMCVFQyVCQiVBOCVFRCU4NSU4RCVFQyU4QSVBNCVFRCU4QSVCOCVFQiVBNSVCQyUyMCVFQyU4MyU5RCVFQyU4NCVCMSVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFQSVCNCU4MCVFQiVBNiVBQyVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyVBMCU4MSVFQyU5QSVBOSVFRCU5NSVCNCVFQyVBMyVCQyVFQiU4QSU5NCUyMCVFQSVCOCVCMCVFQiU4QSVBNSVFQyU5RCU4NCUyMCVFQSVCMCU4MCVFQyVBNyU4NCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFRCU5NCU4NCVFQiVBMCU4OCVFQyU5RSU4NCVFQyU5QiU4QyVFRCU4MSVBQyVFQiVBNSVCQyUyMCVFQyVBMCU5QyVFQSVCMyVCNSVFRCU5NSU5QyVFQiU4QiVBNC4lMjAlRUMlOUQlQjQlRUIlQTUlQkMlMjAlRUQlODUlOEMlRUMlOEElQTQlRUQlOEElQjglMjAlRUMlQkIlQTglRUQlODUlOEQlRUMlOEElQTQlRUQlOEElQjglMjAlRUQlOTQlODQlRUIlQTAlODglRUMlOUUlODQlRUMlOUIlOEMlRUQlODElQUMlRUIlOUQlQkMlRUElQjMlQTAlMjAlRUIlQjYlODAlRUIlQTUlQjglRUIlOEIlQTQlMjIlMkMlMjJtYXJrcyUyMiUzQSU1QiU1RCUyQyUyMnNlbGVjdGlvbnMlMjIlM0ElNUIlNUQlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjI2MjY2ZjQxYjlhMDI0N2IzYWMzZTYwYzEyMTI2NTU3ZSUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjk0Y2I5MTIyMDQyNjRjNGRiNTAyOGYyZTc4YTc3YWM4JTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYTY4NDc5ZGEzNDIwNDA0MDk0OTQ4NDlkYzI1OWRjMDElMjIlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjIzYzUwZWYxYWFkNTA0MjNiYmRiOGZhYmE5NDMwYTM1OCUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjQ3YmE4NGZmNjM0MDRlZDk5ZDI5MmU0NGE3MTQwZDc0JTIyJTdE&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;스프링은 테스트가 사용하는 컨텍스트를 캐싱해서 여러 테스트에서 하나의 컨텍스트를 공유할 수 있는 방법을 제공한다. 이러한 컨텍스트 공유(캐싱)을 사용해서 테스트 속도를 개선해보겠다.&lt;/span&gt;&lt;/div&gt;
&lt;h3 data-slate-fragment=&quot;JTdCJTIyb2JqZWN0JTIyJTNBJTIyZG9jdW1lbnQlMjIlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJoZWFkaW5nLTElMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMiVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFQyVCQiVBOCVFRCU4NSU4RCVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFRCU5NCU4NCVFQiVBMCU4OCVFQyU5RSU4NCVFQyU5QiU4QyVFRCU4MSVBQyUyMiUyQyUyMm1hcmtzJTIyJTNBJTVCJTVEJTJDJTIyc2VsZWN0aW9ucyUyMiUzQSU1QiU1RCU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMmQzMmQwNTQ4MzZiYzRlMTZiNGRlMDEzMzlhNzcxZmU0JTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyNWJiYjEzNzQ4YTk2NDNiMmI1NGQ2YmMwMGZmNzRjYjglMjIlN0QlMkMlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJsaXN0LXVub3JkZXJlZCUyMiUyQyUyMmlzVm9pZCUyMiUzQWZhbHNlJTJDJTIyZGF0YSUyMiUzQSU3QiU3RCUyQyUyMm5vZGVzJTIyJTNBJTVCJTdCJTIyb2JqZWN0JTIyJTNBJTIyYmxvY2slMjIlMkMlMjJ0eXBlJTIyJTNBJTIybGlzdC1pdGVtJTIyJTJDJTIyaXNWb2lkJTIyJTNBZmFsc2UlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJwYXJhZ3JhcGglMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMiVFQyU4QSVBNCVFRCU5NCU4NCVFQiVBNyU4MSVFQyU5RCU4MCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyU4MiVBQyVFQyU5QSVBOSVFQiU5MCU5OCVFQiU4QSU5NCUyMCVFQyU5NSVBMCVFRCU5NCU4QyVFQiVBNiVBQyVFQyVCQyU4MCVFQyU5RCVCNCVFQyU4NSU5OCUyMCVFQyVCQiVBOCVFRCU4NSU4RCVFQyU4QSVBNCVFRCU4QSVCOCVFQiVBNSVCQyUyMCVFQyU4MyU5RCVFQyU4NCVCMSVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFQSVCNCU4MCVFQiVBNiVBQyVFRCU5NSU5OCVFQSVCMyVBMCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCVFQyU5NyU5MCUyMCVFQyVBMCU4MSVFQyU5QSVBOSVFRCU5NSVCNCVFQyVBMyVCQyVFQiU4QSU5NCUyMCVFQSVCOCVCMCVFQiU4QSVBNSVFQyU5RCU4NCUyMCVFQSVCMCU4MCVFQyVBNyU4NCUyMCVFRCU4NSU4QyVFQyU4QSVBNCVFRCU4QSVCOCUyMCVFRCU5NCU4NCVFQiVBMCU4OCVFQyU5RSU4NCVFQyU5QiU4QyVFRCU4MSVBQyVFQiVBNSVCQyUyMCVFQyVBMCU5QyVFQSVCMyVCNSVFRCU5NSU5QyVFQiU4QiVBNC4lMjAlRUMlOUQlQjQlRUIlQTUlQkMlMjAlRUQlODUlOEMlRUMlOEElQTQlRUQlOEElQjglMjAlRUMlQkIlQTglRUQlODUlOEQlRUMlOEElQTQlRUQlOEElQjglMjAlRUQlOTQlODQlRUIlQTAlODglRUMlOUUlODQlRUMlOUIlOEMlRUQlODElQUMlRUIlOUQlQkMlRUElQjMlQTAlMjAlRUIlQjYlODAlRUIlQTUlQjglRUIlOEIlQTQlMjIlMkMlMjJtYXJrcyUyMiUzQSU1QiU1RCUyQyUyMnNlbGVjdGlvbnMlMjIlM0ElNUIlNUQlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjI2MjY2ZjQxYjlhMDI0N2IzYWMzZTYwYzEyMTI2NTU3ZSUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjk0Y2I5MTIyMDQyNjRjNGRiNTAyOGYyZTc4YTc3YWM4JTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYTY4NDc5ZGEzNDIwNDA0MDk0OTQ4NDlkYzI1OWRjMDElMjIlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjIzYzUwZWYxYWFkNTA0MjNiYmRiOGZhYmE5NDMwYTM1OCUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjQ3YmE4NGZmNjM0MDRlZDk5ZDI5MmU0NGE3MTQwZDc0JTIyJTdE&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;개선&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 테스트마다 컨텍스트가 뜨는 부분은 크게 2 부분이었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;WebMvc테스트는 전부 다 뜨고&lt;/li&gt;
&lt;li&gt;AuthService만 다른 service테스트에 없는 mock이 있어서 다시 뜬다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (22).png&quot; data-origin-width=&quot;383&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8jaC8/btrN5bMNJiu/twocPE76q1IklPgwPeBlKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8jaC8/btrN5bMNJiu/twocPE76q1IklPgwPeBlKK/img.png&quot; data-alt=&quot;@MockBean OAuthClient&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8jaC8/btrN5bMNJiu/twocPE76q1IklPgwPeBlKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8jaC8%2FbtrN5bMNJiu%2FtwocPE76q1IklPgwPeBlKK%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;383&quot; height=&quot;266&quot; data-filename=&quot;Untitled (22).png&quot; data-origin-width=&quot;383&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@MockBean OAuthClient&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 WebMvc테스트에서 전부 같은 빈을 띄워주도록 아래처럼 변경을 했다. Service Test도 마찬가지로 빈들을 통합시켜주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1665212817097&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AutoConfigureRestDocs
@WebMvcTest({
MemberController.class,
ArticleController.class,
TempArticleController.class,
AuthController.class,
TagController.class,
CommentController.class,
LikeController.class,
VoteController.class
})
public abstract class ControllerTest {

@MockBean
protected ArticleService articleService;

@MockBean
protected JwtTokenProvider jwtTokenProvider;

@MockBean
protected TempArticleService tempArticleService;

@MockBean
protected CommentService commentService;

@MockBean
protected LikeService likeService;

@MockBean
protected MemberService memberService;

@MockBean
protected TagService tagService;

@MockBean
protected VoteService voteService;

@MockBean
protected AuthService authService;

@Autowired
protected MockMvc mockMvc;

@Autowired
protected ObjectMapper objectMapper;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 빈들을 통합해서 결론적으로는 총 4번만 컨텍스트가 띄우도록 했다. 인수 테스트, 컨트롤러 테스트, 서비스 테스트 그리고 레포지토리 테스트 총 4번만 컨텍스트가 띄워진다. 아래의 사진을 보면 체감을 느낄 수 있었다.&lt;/p&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;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9p7j9/btrN66KYmdJ/BxzDI0bzlBk52kkPKRV7Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9p7j9/btrN66KYmdJ/BxzDI0bzlBk52kkPKRV7Hk/img.png&quot; data-alt=&quot;최적화 효과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9p7j9/btrN66KYmdJ/BxzDI0bzlBk52kkPKRV7Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9p7j9%2FbtrN66KYmdJ%2FBxzDI0bzlBk52kkPKRV7Hk%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;593&quot; height=&quot;550&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최적화 효과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 테스트 빌드 시간과 비교해보면 평균 8초가량이 줄어든 것을 알 수 있다!!!!!&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;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토비의 스프링&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bperhaps.tistory.com/entry/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%97%AC%ED%96%89%EA%B8%B0-4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://bperhaps.tistory.com/entry/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%97%AC%ED%96%89%EA%B8%B0-4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>최적화</category>
      <category>컨텍스트 캐싱</category>
      <category>테스트 코드</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/144</guid>
      <comments>https://giron.tistory.com/144#entry144comment</comments>
      <pubDate>Sat, 8 Oct 2022 16:30:39 +0900</pubDate>
    </item>
    <item>
      <title>서블릿의 요청 처리 과정</title>
      <link>https://giron.tistory.com/143</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;현재 우아한테크코스에서는 &lt;b&gt;톰캣구현하기 미션&lt;/b&gt;을 진행중이다. 해당 미션을 진행하면서 서블릿에 대해 정리를 해보려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서블릿&lt;/h3&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;: 싱글톤 패턴의 경우 객체로 딱 한 번만 생성할 수 있도록 클래스 내부적으로 처리해놓음.&lt;/li&gt;
&lt;li&gt;톰캣이 구조상 서블릿을 한번만 생성할 뿐임.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVHQ59/btrM4QRyJQv/S8TGeUJCDcbPKPd4aQRsJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVHQ59/btrM4QRyJQv/S8TGeUJCDcbPKPd4aQRsJ0/img.png&quot; data-alt=&quot;다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVHQ59/btrM4QRyJQv/S8TGeUJCDcbPKPd4aQRsJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVHQ59%2FbtrM4QRyJQv%2FS8TGeUJCDcbPKPd4aQRsJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;263&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy3JEN/btrM8IxuOkt/sL5rnkuPV0UdJfUIJK0VD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy3JEN/btrM8IxuOkt/sL5rnkuPV0UdJfUIJK0VD0/img.png&quot; data-alt=&quot;디스페처 서블릿&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy3JEN/btrM8IxuOkt/sL5rnkuPV0UdJfUIJK0VD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy3JEN%2FbtrM8IxuOkt%2FsL5rnkuPV0UdJfUIJK0VD0%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;353&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디스페처 서블릿&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HandlerAdapter와 HandlerMapping으로 나눈 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어댑터 패턴이 무엇인가 하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull; 현재 사용하고 있는 라이브러리가 더 이상 요구에 부합하지 않아 재 작성하거나, 다른 라이브러리를 사용해야 할 때가 있다. 다른 라이브러리를 사용하는 경우 Adapter 패턴을 이용해 기존 코드를 가능한 적게 변경하면서 새로운 라이브러리로 교체할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 기존의 인터페이스 기반의 Controller를 제거하고 어노테이션기반 Controller로 동작하게 만들 때 사용할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애노테이션 기반인 @Controller를 통해서 구현한다.&lt;/li&gt;
&lt;li&gt;HttpRequestHandler를 사용한다.&lt;/li&gt;
&lt;li&gt;Controller인터페이스를 사용한다.(2.5 버전까지만 사용하고 더 이상 사용하지 않는다.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Controller 인터페이스를 사용하는 방식에서 변환이 되면서 해당 코드를 어노테이션기반과 RequestHandler기반으로 바꾸기 위해서이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다이어그램 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;HandlerAdapterRegistry&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;객체&lt;/b&gt;를 반환하지만 &lt;span&gt;&lt;b&gt;HadnlerMappingRegistry&lt;/b&gt;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Optional을 반환&lt;/b&gt;한다. 둘다 내부에서 예외처리를 하고 객체를 반환하지 않고 각각 다르게 반환하는 이유가 궁금했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 궁금증을 &quot;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;존재하지 않는 리소스에 접근할경우 404를 반환해주세요~&quot;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;라는 리뷰를 받고 해결한것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자가 생각하기에 이러한 이유는 HandlerMappingRegistry는 핸들러가 없다면 예외 페이지를 보여주는 것과 같이 예외처리를 해줘야 한다. 따라서 Optional로 반환해서 Handler가 있는지 없는지를 dispatcherServlet에게 알려야 한다고 생각했다.&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;아래 코드처럼 redirect시키려면 dispatcher의 service에서 실행해야 하므로 Optional을 반환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (20).png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgEfML/btrM5PLFeIR/KyzuwaUndW9RHVUpCq0MIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgEfML/btrM5PLFeIR/KyzuwaUndW9RHVUpCq0MIK/img.png&quot; data-alt=&quot;구현한 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgEfML/btrM5PLFeIR/KyzuwaUndW9RHVUpCq0MIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgEfML%2FbtrM5PLFeIR%2FKyzuwaUndW9RHVUpCq0MIK%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;904&quot; height=&quot;497&quot; data-filename=&quot;Untitled (20).png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구현한 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 HandlerMappingRegistry가 response까지 매개변수로 받아서 처리할 수 있다. 대신 이러면 객체의 책임이 너무 HandlerMappingRegistry로 넘어간다고 볼 수 있다. HandlerMappingRegistry는 핸들러를 찾는 역할만 하고 없다면 disptcherServlet에서 service에서 페이지를 반환하도록 처리했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bN2NZf/btrM8gHYXBO/17eeWpnkwfZPcyZ8nXzJFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bN2NZf/btrM8gHYXBO/17eeWpnkwfZPcyZ8nXzJFK/img.png&quot; data-alt=&quot;최종 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN2NZf/btrM8gHYXBO/17eeWpnkwfZPcyZ8nXzJFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbN2NZf%2FbtrM8gHYXBO%2F17eeWpnkwfZPcyZ8nXzJFK%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;666&quot; height=&quot;371&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;요청 처리 과정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿과 서블릿 컨테이너 요청 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣: 서블릿 컨테이너 or WAS&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트로부터 요청이 들어오면 웹 컨테이너(톰캣)이 요청을 받는다.&lt;/li&gt;
&lt;li&gt;로그인을 처리하는 서블릿을 개발자가 작성했으면, 컴파일 후 만들어진 클래스파일을 웹 컨테이너(WAS)가 서블릿 객체로 만들어 논다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인을 처리하는 서블릿은 하나만 만들어진다.&lt;/li&gt;
&lt;li&gt;로그아웃을 처리하는 서블릿은 하나만 만들어진다.&lt;/li&gt;
&lt;li&gt;하지만 싱글톤은 아니다. 내부가 싱글톤 구조가 아니다. 구조상 한 번만 만들 뿐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;요청을 가지고 Request, Response 객체를 만든다.&lt;/li&gt;
&lt;li&gt;URL에 맞춰 적절한 서블릿을 찾아서 service()메서드를 실행한다. 파라미터로 Request, Response를 보내준다.&lt;/li&gt;
&lt;li&gt;서블릿 객체는 하나지만 여러 스레드가 작동하므로 가능하다.&lt;/li&gt;
&lt;li&gt;response객체로 컨테이너에게 돌려주면 컨테이너는 클라이언트에게 준다.&lt;/li&gt;
&lt;li&gt;서블릿 객체는 유지되지만, 쓰레드는 반납하고 Request, Response객체는 JVM에 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스패처 서블릿의 흐름은 아래 링크에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://giron.tistory.com/111&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://giron.tistory.com/111&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1664213479896&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;디스패처 서블릿의 흐름&quot; data-og-description=&quot;서블릿을 만들려면 HttpServlet 클래스를 상속받으면 되나? - 서블릿 프로그래밍의 핵심은 Servlet 인터페이스를 이해해야 한다. 서블릿 웹 브라우저 웹 서버 웹 애플리케이션에서 웹서버와 웹 애플&quot; data-og-host=&quot;giron.tistory.com&quot; data-og-source-url=&quot;https://giron.tistory.com/111&quot; data-og-url=&quot;https://giron.tistory.com/111&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ccra6d/hyPVmSUird/BZXW5GSGHARXMOKJfojTi1/img.png?width=800&amp;amp;height=601&amp;amp;face=0_0_800_601,https://scrap.kakaocdn.net/dn/bBbaup/hyPVjhBkGR/xfZz7Oj9pTR7L3PJlXeEt1/img.png?width=800&amp;amp;height=601&amp;amp;face=0_0_800_601,https://scrap.kakaocdn.net/dn/b7qFL0/hyPVtkfexo/Vz31fKAKyNdsh5eHSB9uy0/img.jpg?width=1080&amp;amp;height=810&amp;amp;face=0_0_1080_810&quot;&gt;&lt;a href=&quot;https://giron.tistory.com/111&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://giron.tistory.com/111&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ccra6d/hyPVmSUird/BZXW5GSGHARXMOKJfojTi1/img.png?width=800&amp;amp;height=601&amp;amp;face=0_0_800_601,https://scrap.kakaocdn.net/dn/bBbaup/hyPVjhBkGR/xfZz7Oj9pTR7L3PJlXeEt1/img.png?width=800&amp;amp;height=601&amp;amp;face=0_0_800_601,https://scrap.kakaocdn.net/dn/b7qFL0/hyPVtkfexo/Vz31fKAKyNdsh5eHSB9uy0/img.jpg?width=1080&amp;amp;height=810&amp;amp;face=0_0_1080_810');&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;서블릿을 만들려면 HttpServlet 클래스를 상속받으면 되나? - 서블릿 프로그래밍의 핵심은 Servlet 인터페이스를 이해해야 한다. 서블릿 웹 브라우저 웹 서버 웹 애플리케이션에서 웹서버와 웹 애플&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;giron.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <category>HadnlerAdapter</category>
      <category>HandlerMapping</category>
      <category>다이어그램</category>
      <category>서블릿</category>
      <category>요청처리과정</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/143</guid>
      <comments>https://giron.tistory.com/143#entry143comment</comments>
      <pubDate>Tue, 27 Sep 2022 02:36:23 +0900</pubDate>
    </item>
    <item>
      <title>SQL Injection 그리고 PreparedStatement</title>
      <link>https://giron.tistory.com/142</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL 인젝션이란?&lt;/h3&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;820&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oGJTZ/btrMnlW8O1Y/BlHK8pdCXbPZOA1EKHZlZK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oGJTZ/btrMnlW8O1Y/BlHK8pdCXbPZOA1EKHZlZK/img.jpg&quot; data-alt=&quot;사례&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oGJTZ/btrMnlW8O1Y/BlHK8pdCXbPZOA1EKHZlZK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoGJTZ%2FbtrMnlW8O1Y%2FBlHK8pdCXbPZOA1EKHZlZK%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;820&quot; height=&quot;430&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사례&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 기법이지만 강력한 공격이어서 반드시 주의해야 한다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예방 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러처리를 잘해서 테이블 정보를 사용자들에게 공개하지 않도록 해야한다. 테이블 정보 노출 및 컬럼 노출로 sql쿼리 작성이 가능하기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방어 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파라미터 바인딩&lt;/b&gt;! 직접 쿼리를 작성하지 않고 파라미터 바인딩을 사용하면 된다. 예시를 들어 설명해보겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SQL Injection에 취약한 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1663382854039&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;statement = connection.createStatemnt();
String query = &quot;SELECT * FROM USERS WHERE name = '&quot; + loginName + &quot;' AND password = '&quot;+loginPassword=&quot;'&quot;;
boolen resultSet = statement.execute(query);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적으로 sql문법은&amp;nbsp; &lt;b&gt;'&lt;/b&gt; &lt;b&gt;'&lt;/b&gt;안에 파라미터를 넣어 문자를 구별한다. 따라서 텍스트 창에 &lt;b&gt;'OR 1=1--&lt;/b&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;b&gt;'&lt;/b&gt; 으로&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앞의 쿼리문을 닫고 or연산으로 1=1과 같이 트루인 로직을 넣어서 공격하는 기법이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SQL Injection 방어 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1663382895761&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String prepareStatement = &quot;SELECT * FROM USERS WHERE name = ? AND password =?;
PreparedStatement preparedStatement = connection.prepareStatement(prepareStatement);
preparedStatement.setString(1, loginName);
preparedStatement.setString(2, loginPassword);&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;h2 data-ke-size=&quot;size26&quot;&gt;파라미터 바인딩을 사용하면 안전한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;preparedStatement.setString(); 의 setString내부를 살펴보겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;org/h2/jdbc/JdbcPreparedStatement.java&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (12).png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brhhmp/btrMjdfV8tL/ivagWd0ev1ukkagQwvlRaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brhhmp/btrMjdfV8tL/ivagWd0ev1ukkagQwvlRaK/img.png&quot; data-alt=&quot;PreparedStatement 내부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brhhmp/btrMjdfV8tL/ivagWd0ev1ukkagQwvlRaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrhhmp%2FbtrMjdfV8tL%2FivagWd0ev1ukkagQwvlRaK%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;990&quot; height=&quot;306&quot; data-filename=&quot;Untitled (12).png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PreparedStatement 내부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;quote내부로 들어가면 아래와 같이 &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;1&quot; data-reactroot=&quot;&quot;&gt;&lt;b&gt;StringUtils.quoteJavaString()&lt;/b&gt;&amp;nbsp;메서드가 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (13).png&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A65Ni/btrMjWxIfgu/2cgPBwHXZzF542KEUQ1Qe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A65Ni/btrMjWxIfgu/2cgPBwHXZzF542KEUQ1Qe1/img.png&quot; data-alt=&quot;quote&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A65Ni/btrMjWxIfgu/2cgPBwHXZzF542KEUQ1Qe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA65Ni%2FbtrMjWxIfgu%2F2cgPBwHXZzF542KEUQ1Qe1%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;438&quot; height=&quot;181&quot; data-filename=&quot;Untitled (13).png&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;quote&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드에 들어가면 아래가 나오고 다시 javaEncode에 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (14).png&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckS2vT/btrMnKP5dZL/ZSmMmettKJMvHzDXB9hKLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckS2vT/btrMnKP5dZL/ZSmMmettKJMvHzDXB9hKLk/img.png&quot; data-alt=&quot;quote 내부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckS2vT/btrMnKP5dZL/ZSmMmettKJMvHzDXB9hKLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckS2vT%2FbtrMnKP5dZL%2FZSmMmettKJMvHzDXB9hKLk%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;822&quot; height=&quot;343&quot; data-filename=&quot;Untitled (14).png&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;quote 내부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 String으로 들어온 값들을 하나씩 돌면서 java스타일에 맞게 인코딩해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (15).png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;827&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2bUbO/btrMlUMfy8E/spjn41fOML7p2FknbHltNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2bUbO/btrMlUMfy8E/spjn41fOML7p2FknbHltNk/img.png&quot; data-alt=&quot;자동 인코딩&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2bUbO/btrMlUMfy8E/spjn41fOML7p2FknbHltNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2bUbO%2FbtrMlUMfy8E%2Fspjn41fOML7p2FknbHltNk%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;790&quot; height=&quot;827&quot; data-filename=&quot;Untitled (15).png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;827&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자동 인코딩&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (16).png&quot; data-origin-width=&quot;356&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/92E1u/btrMlec7H4e/11482rlSpWowYIS3W90cr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/92E1u/btrMlec7H4e/11482rlSpWowYIS3W90cr0/img.png&quot; data-alt=&quot;자동 인코딩&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/92E1u/btrMlec7H4e/11482rlSpWowYIS3W90cr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F92E1u%2FbtrMlec7H4e%2F11482rlSpWowYIS3W90cr0%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;356&quot; height=&quot;718&quot; data-filename=&quot;Untitled (16).png&quot; data-origin-width=&quot;356&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자동 인코딩&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 자동으로 sql injection에 대해서 막아준다. 즉, Prepared Statement를 사용하면 인자를 넣어주기 전의 쿼리를 DBMS가 미리 컴파일하여 대기하므로 이후, 인자에 대해서는 쿼리가 아닌 단순 문자열로 인식하기 때문에 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 직접 작성하지 말고 파라미터 바인딩을 이용하자!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 에러가 나올 때, 어떤 DBMS를 쓰는지 노출하지 않는 것도 중요하다. DBMS에 따라 쿼리 문법이 조금씩 다르기 때문이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Statement vs PreparedStatement&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 차이는 캐시 사용여부이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-27 오후 2.38.18.png&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nycZB/btscS5bgKgq/YkHKfSZFOF0CpIEWBrBK30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nycZB/btscS5bgKgq/YkHKfSZFOF0CpIEWBrBK30/img.png&quot; data-alt=&quot;sql 쿼리 처리 순서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nycZB/btscS5bgKgq/YkHKfSZFOF0CpIEWBrBK30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnycZB%2FbtscS5bgKgq%2FYkHKfSZFOF0CpIEWBrBK30%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;1744&quot; height=&quot;390&quot; data-filename=&quot;스크린샷 2023-04-27 오후 2.38.18.png&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;sql 쿼리 처리 순서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;statement를 사용하면 매 번 위의 과정을 반복하면서 이루어지지만 preparedStatement는 처음에 모든 과정이 일어날 때, parse의 과정 이후를 캐시에 저장하므로 같은 쿼리문을 재사용할 때, 해당 캐시를 사용하므로 성능상 좋다. (전달되는 파라미터의 값이 달라도 같은 쿼리로 인식해서 처리하므로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 동일한 sql문을 반복 사용할 때 좋다. 또한 내부적으로 바인딩 처리할 때, sql injection을 막아주므로 좋다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;statement&lt;/h4&gt;
&lt;pre id=&quot;code_1663383780513&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String sql = &quot;SELECT NAME, AGE FROM TABLE WHERE USERID = &quot; + userID
Statement stmt = conn.credateStatment();
ResultSet result = stmt.executeQuery(sqlstr);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;prepared statement&lt;/h4&gt;
&lt;pre id=&quot;code_1663383800491&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String sql = &quot;SELECT NAME, AGE FROM TABLE WHERE userID = ?&quot;
PreparedStatement stmt = conn.prepareStatement(sql);
pstmt.setInt(1, userID);
ResultSet rst = pstmt.executeQuery();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;jpql&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jpql을 사용할 때도 아래와 같이 사용한다면 똑같이 SQL Injection위험이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1663383823243&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public List&amp;lt;AccountDTO&amp;gt; unsafeJpaFindAccountsByCustomerId(String customerId) {    
    String jql = &quot;from Account where customerId = '&quot; + customerId + &quot;'&quot;;        
    TypedQuery&amp;lt;Account&amp;gt; q = em.createQuery(jql, Account.class);        
    return q.getResultList()
      .stream()
      .map(this::toAccountDTO)
      .collect(Collectors.toList());        
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래와 같이 파라미터 바인딩을 사용하자&lt;/p&gt;
&lt;pre id=&quot;code_1663383841848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String jql = &quot;from Account where customerId = :customerId&quot;;
TypedQuery&amp;lt;Account&amp;gt; q = em.createQuery(jql, Account.class)
  .setParameter(&quot;customerId&quot;, customerId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 바인딩 문은 values의 값에만 사용할 수 있다. table같은 경우는 할 수없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 동적으로 table 명을 바꾸려는 문장은 런타임 에러가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1663383877505&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// This WILL NOT WORK !!!
PreparedStatement p = c.prepareStatement(&quot;select count(*) from ?&quot;);
p.setString(1, tableName);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1663383886452&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// This WILL NOT WORK EITHER !!!
String jql = &quot;select count(*) from :tableName&quot;;
TypedQuery q = em.createQuery(jql,Long.class)
  .setParameter(&quot;tableName&quot;, tableName);
return q.getSingleResult();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The main reason behind this is the very nature of a prepared statement: database servers use them to cache the query plan required to pull the result set, which usually is the same for any possible value. This is not true for table names and other constructs available in the SQL language such as columns used in an&amp;nbsp;&lt;br /&gt;order by&amp;nbsp;clause.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주된 이유는 preparedStatement의 특성 때문이다. DB서버는 이것들을 캐싱하여 사용한다. 하지만 테이블의 이름을 동적으로 두면 캐싱할 수 없기 때문에 table의 이름을 바인딩으로 넣을 순 없다.&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://www.baeldung.com/sql-injection&quot;&gt;https://www.baeldung.com/sql-injection&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/142</guid>
      <comments>https://giron.tistory.com/142#entry142comment</comments>
      <pubDate>Sat, 17 Sep 2022 12:10:17 +0900</pubDate>
    </item>
    <item>
      <title>테스트 코드의 어노테이션을 줄여보자 그리고 테스트 컨텍스트 캐싱의 이점을 얻자</title>
      <link>https://giron.tistory.com/141</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트가 어느 정도 진행되면서 Test위에 붙는 Import도 늘어났다. 아래 사진처럼 매번 Repository 테스트를 할 때마다 붙여줘야 하는데 불편해서 어노테이션을 만들어서 정리를 해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;467&quot; data-origin-height=&quot;87&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8zQqf/btrLZz4pvcl/mRUFf8QKxOnyGypUR7jLFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8zQqf/btrLZz4pvcl/mRUFf8QKxOnyGypUR7jLFK/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8zQqf/btrLZz4pvcl/mRUFf8QKxOnyGypUR7jLFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8zQqf%2FbtrLZz4pvcl%2FmRUFf8QKxOnyGypUR7jLFK%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;467&quot; height=&quot;87&quot; data-origin-width=&quot;467&quot; data-origin-height=&quot;87&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 포스팅 할만한 정도로 대단할 건 없지만 앞으로도 프로젝트를 진행하면 자주 적용할 것 같아서 기록해두려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;459&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wEHxg/btrL3SvcUPk/Qm2rQUnYWmsn6B1Z0Ezjuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wEHxg/btrL3SvcUPk/Qm2rQUnYWmsn6B1Z0Ezjuk/img.png&quot; data-alt=&quot;적용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wEHxg/btrL3SvcUPk/Qm2rQUnYWmsn6B1Z0Ezjuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwEHxg%2FbtrL3SvcUPk%2FQm2rQUnYWmsn6B1Z0Ezjuk%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;459&quot; height=&quot;132&quot; data-origin-width=&quot;459&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;적용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메타 어노테이션을 활용하여 위와 같이 작성해주면 끝!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FCeUa/btrL35gPZk8/mbNKPIlp5QYMFF8xOphRZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FCeUa/btrL35gPZk8/mbNKPIlp5QYMFF8xOphRZk/img.png&quot; data-alt=&quot;적용2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FCeUa/btrL35gPZk8/mbNKPIlp5QYMFF8xOphRZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFCeUa%2FbtrL35gPZk8%2FmbNKPIlp5QYMFF8xOphRZk%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;286&quot; height=&quot;62&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;적용2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 @Repository만 작성하면 간편하게 적용이 가능하다. 또한 스프링은 테스트 시 매번 컨텍스트를 띄우지 않고 테스트가 사용하는 컨텍스트를 캐싱해서 여러 테스트가 1개의 (애플리케이션) 컨텍스트를 공유하는 방법을 제공한다. 즉, &lt;b&gt;테스트마다 동일한 컨텍스트를 사용하면 컨텍스트가 1번만 올라가므로 테스트 성능에도 좋다.&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) &lt;span&gt;@Repository만 작성하면 간편하게 적용이 가능하다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) (레포테스트기준)&lt;b&gt;컨텍스트가 1번만 올라가므로 테스트 성능에도 좋다.&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;&amp;nbsp;Java의&amp;nbsp;어노테이션은&amp;nbsp;크게&lt;b&gt; built-in&amp;nbsp;어노테이션&lt;/b&gt;과&amp;nbsp;&lt;b&gt;Meta 어노테이션&lt;/b&gt;이 존재한다. 메타 데이터의 메타와 마찬가지로 어노테이션을 위한 어노테이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;built-in어노테이션은 흔히 아는 @Overried, @Deprecated, @SuppressWarnings 등이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타 어노테이션&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Retention&lt;/h4&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;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjAAkl/btrL3zo6H9U/rf3O7LOc2GyhRzpjabKJe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjAAkl/btrL3zo6H9U/rf3O7LOc2GyhRzpjabKJe1/img.png&quot; data-alt=&quot;Retention&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjAAkl/btrL3zo6H9U/rf3O7LOc2GyhRzpjabKJe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjAAkl%2FbtrL3zo6H9U%2Frf3O7LOc2GyhRzpjabKJe1%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;240&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Retention&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 어노테이션의 정보를 어느 범위까지 유지할 것인지를 설정한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RetentionPolicy.SOURCE: 컴파일 전까지만 유효하며 컴파일 이후에는 사라짐&lt;/li&gt;
&lt;li&gt;RetentionPolicy.CLASS: 컴파일러가 클래스를 참조할 때까지 유효함&lt;/li&gt;
&lt;li&gt;RetentionPolicy.RUNTIME: Reflection을 사용하여 컴파일 이후에도 JVM에 의해 계속 참조가 가능함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Target&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QEtz3/btrL3zQatX4/kHdUKrTqtwKc2Zhgvyz4d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QEtz3/btrL3zQatX4/kHdUKrTqtwKc2Zhgvyz4d0/img.png&quot; data-alt=&quot;Target&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QEtz3/btrL3zQatX4/kHdUKrTqtwKc2Zhgvyz4d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQEtz3%2FbtrL3zQatX4%2FkHdUKrTqtwKc2Zhgvyz4d0%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;683&quot; height=&quot;451&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Target&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;해당 어노테이션이 사용되는 위치를 결정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ElementType.PACKAGE : 패키지 선언 시 사용한다.&lt;/li&gt;
&lt;li&gt;ElementType.TYPE : 클래스, 인터페이스, 이넘 선언 시 사용한다.&lt;/li&gt;
&lt;li&gt;ElementType.CONSTRUCTOR : 생성자 선언시 사용한다.&lt;/li&gt;
&lt;li&gt;ElementType.FIELD : 맴버 변수 선언 시 사용한다.&lt;/li&gt;
&lt;li&gt;ElementType.METHOD : 메소드 선언 시 사용한다.&lt;/li&gt;
&lt;li&gt;ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언 시 사용한다.&lt;/li&gt;
&lt;li&gt;ElementType.LOCAL_VARIABLE : 지역 변수 선언시 사용한다.&lt;/li&gt;
&lt;li&gt;ElementType.TYPE_PARAMETER : 매개 변수 타입 선언 시 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ElementType 내부에 설명이 잘 되어있어서 읽으면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Documented: JavaDoc 생성 시 Document에 포함되도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Inherited: 해당 어노테이션을 하위 클래스까지 적용한다.&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>메타어노테이션</category>
      <category>어노테이션</category>
      <category>어노테이션 줄이기</category>
      <category>테스트컨텍스트 캐싱</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/141</guid>
      <comments>https://giron.tistory.com/141#entry141comment</comments>
      <pubDate>Tue, 13 Sep 2022 22:54:08 +0900</pubDate>
    </item>
    <item>
      <title>AOP에 대한 사실과 오해 그런데 트랜잭션을 사알짝 곁들인..</title>
      <link>https://giron.tistory.com/140</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 조금 각색해서 우아한테크코스 블로그(&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2022-11-07-transaction-aop-fact-and-misconception/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테코블&lt;/a&gt;)에 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 사용할 때, 아래의 사진처럼 private 메서드에 걸면 컴파일 에러가 나오는 것을 확인할 수가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (6).png&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bElpLy/btrLQxYBDEw/6GhLz1QQ6e3I8fkkL7Xkb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bElpLy/btrLQxYBDEw/6GhLz1QQ6e3I8fkkL7Xkb0/img.png&quot; data-alt=&quot;compile 에러&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bElpLy/btrLQxYBDEw/6GhLz1QQ6e3I8fkkL7Xkb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbElpLy%2FbtrLQxYBDEw%2F6GhLz1QQ6e3I8fkkL7Xkb0%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;365&quot; height=&quot;153&quot; data-filename=&quot;Untitled (6).png&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;compile 에러&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이가 알려주는 메시지를 보면 private 만 사용하지 않으면 된다고 한다. 이 이유에 대해서는 spring 2.5버전 이후부터는 default로 CGLIB을 사용하므로 &lt;b&gt;상속을 통해 프록시&lt;/b&gt;를 구현한다. 하지만 &lt;b&gt;private메서드는 상속이 불가능&lt;/b&gt;하기 때문에 프록시를 만들 수 없기 때문이다. 하지만 스프링 공식문서를 보면 public 이외의 모든 메서드는 트랜잭션이 적용되지 않는다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (7).png&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bss0Zw/btrLMxFgTd6/AI86A1ioNusLYX4Tf2uNrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bss0Zw/btrLMxFgTd6/AI86A1ioNusLYX4Tf2uNrk/img.png&quot; data-alt=&quot;공식 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bss0Zw/btrLMxFgTd6/AI86A1ioNusLYX4Tf2uNrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbss0Zw%2FbtrLMxFgTd6%2FAI86A1ioNusLYX4Tf2uNrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;210&quot; data-filename=&quot;Untitled (7).png&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 protected로 하면 컴파일단에선 예외가 잡히지 않는다. 왜냐하면 프록시는 만들어지기 때문이다. 하지만 스프링 공식문서를 보면 트랜잭션이 적용이 되지 않는다고 한다. 적용하려면 aspectJ의 compile-time or load-time weaving을 적용하면 된다고 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주의할 점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public이외의 메서드는 AOP가 걸리지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;동일한 클래스(빈)&lt;/i&gt;&lt;/b&gt; 내에서 @Transanctional이&amp;nbsp;&lt;b&gt;선언되지 않은 메서드에서&lt;/b&gt; @Transactional이&amp;nbsp;&lt;b&gt;선언된 메서드를 호출&lt;/b&gt;해도&amp;nbsp;&lt;b&gt;트랜잭션이 적용되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;@Transactional(propagation=Propagation.REQUIRES_NEW)사용할 때,&amp;nbsp;&lt;b&gt;&lt;i&gt;동일한 클래스(빈)의&lt;/i&gt;&lt;/b&gt; 메서드끼리 호출하면 새로 생성되지 않음. 반드시&amp;nbsp;&lt;b&gt;다른 클래스 메서드를 호출해야 함.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데 왜?? 안되는 걸까?&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1번 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택 오버플로우에서 참고하였다. 이유는 JDK와 CGLIB의 구분 없이 일관되게 적용하기 위해서라고 한다! &lt;span&gt; &lt;/span&gt;&lt;span&gt; 왜냐하면 JDK 동적 프록시는 인터페이스를 기반으로 한 프록시를 만든다. 따라서 protected를 사용하면 프록시를 만들 수가 없기 때문에 JDK 동적 프록시까지 고려하여 public이 아닌 메서드에서는 aop가 걸리지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://stackoverflow.com/questions/34197964/why-doesnt-springs-transactional-work-on-protected-methods&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/34197964/why-doesnt-springs-transactional-work-on-protected-methods&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662875489300&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;Why doesn't Spring's @Transactional work on protected methods?&quot; data-og-description=&quot;From Does Spring @Transactional attribute work on a private method? When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/34197964/why-doesnt-springs-transactional-work-on-protected-methods&quot; data-og-url=&quot;https://stackoverflow.com/questions/34197964/why-doesnt-springs-transactional-work-on-protected-methods&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bRz4Ul/hyPLhCOvJR/V8h0A2JOtAA9iuONleK6QK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/bwecUe/hyPLl6hwPH/eCu124KHKTfI289GUunJVK/img.png?width=572&amp;amp;height=266&amp;amp;face=0_0_572_266&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/34197964/why-doesnt-springs-transactional-work-on-protected-methods&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/34197964/why-doesnt-springs-transactional-work-on-protected-methods&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bRz4Ul/hyPLhCOvJR/V8h0A2JOtAA9iuONleK6QK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/bwecUe/hyPLl6hwPH/eCu124KHKTfI289GUunJVK/img.png?width=572&amp;amp;height=266&amp;amp;face=0_0_572_266');&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;Why doesn't Spring's @Transactional work on protected methods?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;From Does Spring @Transactional attribute work on a private method? When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2, 3번 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 공식문서에 나와있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with&amp;nbsp;@Transactional . Also, the proxy must be fully initialized to provide the expected behavior, so you should not rely on this feature in your initialization code&amp;thinsp;&amp;mdash;&amp;thinsp;for example, in a&amp;nbsp;@PostConstruct &amp;nbsp;method.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 프록시를 통해 들어오는&lt;b&gt; 외부 메서드 호출만 인터셉트된다.&lt;/b&gt; 즉, Spring AOP 는 외부 메서드의 호출만 인터셉트한다는 것이다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 프록시의 내부 빈에서 프록시를 호출했기 때문이다. 사진으로 보면 이해가 도움이 될 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (8).png&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J2ZrV/btrLTBztdz7/26Y40PPmmM3uEcptzKIG0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J2ZrV/btrLTBztdz7/26Y40PPmmM3uEcptzKIG0K/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J2ZrV/btrLTBztdz7/26Y40PPmmM3uEcptzKIG0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ2ZrV%2FbtrLTBztdz7%2F26Y40PPmmM3uEcptzKIG0K%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;1055&quot; height=&quot;686&quot; data-filename=&quot;Untitled (8).png&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;progress()가 일반 메서드일 때, init()을 호출해도 프록시는 이미 프록시 객체 안에 있기 때문에 프록시를 인터셉트할 수 없는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하지만 위의 3가지 경우는 AspectJ를 사용하면 가능하다는데 왜 그럴까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 공식문서를 잘 보자..&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;self-invocation(in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime&amp;hellip;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟 내에서 타겟의 다른 메서드를 호출할 때, 런타임에 실제 트랜잭션이 작동하지 않는다. 즉, 런타임 시점에 작동은 안 하지만 이것을 컴파일 시점에 적용하면 된다는 것이다. 이미 프록시로 감싸졌으므로, 따라서 AspectJ를 사용한 바이트코드 조작은 프록시로 감싸기 전에 적용되므로 같은 빈 내부에서도 호출이 가능한 것 같다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, springAOP는 동적프록시를 사용하여 런타임 시점에 위빙을 하지만 AspectJ는 바이트코드를 조작하여 컴파일 시점에 위빙을 하기 때문에 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AOP 용어&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Target : 어떤 &lt;b&gt;대상&lt;/b&gt;에게 부가할것인지&lt;/li&gt;
&lt;li&gt;Advice: 부가기능을 &lt;b&gt;언제&lt;/b&gt; 부여할 것인가. (Before, After..)&lt;/li&gt;
&lt;li&gt;JoinPoint: 어디에 적용할 것인가(적용 가능 대상): 메서드 , 필드 등 (스프링AOP는 메서드만 가능)&lt;/li&gt;
&lt;li&gt;pointCut: &lt;b&gt;실제 advice가 적용되는 곳 &lt;/b&gt;(joinPoint 대상들 중에 적용)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (10).png&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;29&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mq1y4/btrLLiPD6PX/Rm6WRPqvOTjt86G5dxZmA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mq1y4/btrLLiPD6PX/Rm6WRPqvOTjt86G5dxZmA1/img.png&quot; data-alt=&quot;aop적용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mq1y4/btrLLiPD6PX/Rm6WRPqvOTjt86G5dxZmA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMq1y4%2FbtrLLiPD6PX%2FRm6WRPqvOTjt86G5dxZmA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;29&quot; data-filename=&quot;Untitled (10).png&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;29&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;aop적용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Around &amp;larr; Advice&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간색이 &amp;larr; pointCut&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (11).png&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edgbOT/btrLPekK4p7/jFkyK2L9ECfH0quiD8Ivsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edgbOT/btrLPekK4p7/jFkyK2L9ECfH0quiD8Ivsk/img.png&quot; data-alt=&quot;어노테이션을 사용한 적용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edgbOT/btrLPekK4p7/jFkyK2L9ECfH0quiD8Ivsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedgbOT%2FbtrLPekK4p7%2FjFkyK2L9ECfH0quiD8Ivsk%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;669&quot; height=&quot;322&quot; data-filename=&quot;Untitled (11).png&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;어노테이션을 사용한 적용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Weaving(위빙)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aspect 모듈을 통해 실제 프록시 객체를 생성하기 위한 과정. 즉 공통 코드를 핵심 로직 코드에 삽입하는 것을 weaving이라고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AspectJ&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴파일 시점에 위빙한다. 위빙 방식은 3가지가 있다.&lt;/li&gt;
&lt;li&gt;성능이 스프링 AOP보다 빠르다.&lt;/li&gt;
&lt;li&gt;스프링에 종속되지 않고 자바 전역에 적용이 가능하다. ex) 메서드, 필드 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;런타임 시점에 동적으로 위빙한다. (JDK 또는 CGLIB 사용)&lt;/li&gt;
&lt;li&gt;AspectJ보다 성능이 안 좋다.&lt;/li&gt;
&lt;li&gt;스프링이 관리하는 빈에서만 적용이 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정하기 편하다. 또한 AspectJ와 다르게 컴파일 시점에 건드리는 게 없어서 각종 라이브러리(Lombok)과 호환성이 뛰어나다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 인터페이스, 인터페이스의 메서드 그리고 클래스, 클래스의 메서드에 정의할 수 있다. 하지만 모두 정의만 한다고 실행되는 것은 아니다. 공식문서에서는 클래스에 트랜잭션을 거는 것을 추천한다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (9).png&quot; data-origin-width=&quot;1171&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMvRYL/btrLLiPDWaF/oJTiffuolHQ21gSsm0ki40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMvRYL/btrLLiPDWaF/oJTiffuolHQ21gSsm0ki40/img.png&quot; data-alt=&quot;공식 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMvRYL/btrLLiPDWaF/oJTiffuolHQ21gSsm0ki40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMvRYL%2FbtrLLiPDWaF%2FoJTiffuolHQ21gSsm0ki40%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;1171&quot; height=&quot;256&quot; data-filename=&quot;Untitled (9).png&quot; data-origin-width=&quot;1171&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK기반 프록시를 실행시켰을 때, 인터페이스를 적용하는 게 좋다고 한다, 왜냐하면 앞선 테코톡에서도 말했지만 스프링에서 CGLIB을 디폴트로 하는 이유도 인터페이스 기반 프록시는 ClassCast Exception 추적이 어렵다. 그래서인지 Aop도 인터페이스에 걸지 말라고 하는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;마무리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션에 대해서 좀 더 알아간 것 같아서 뿌듯하다. 테코톡을 준비하면서 CGLIB과 JDK를 공부했던게 이렇게 도움이 될줄은 몰랐다. 테코톡을 준비했던 덕분에 쉽게 이해할 수 있었던 것 같다!!!&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662875977955&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;Data Access&quot; data-og-description=&quot;The Data Access Object (DAO) support in Spring is aimed at making it easy to work with data access technologies (such as JDBC, Hibernate, or JPA) in a consistent way. This lets you switch between the aforementioned persistence technologies fairly easily, a&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/PqSb8/hyPLdHbAY1/4dA6JL1acItnXkWTUOUws1/img.png?width=916&amp;amp;height=397&amp;amp;face=0_0_916_397,https://scrap.kakaocdn.net/dn/u1J4n/hyPJ3e7K9A/gfVuKGzLL585r4xq4k3wK0/img.png?width=800&amp;amp;height=341&amp;amp;face=0_0_800_341,https://scrap.kakaocdn.net/dn/dl3zT4/hyPLee1bcu/1ie93Td6lVk9OKadKxmhQ1/img.png?width=800&amp;amp;height=276&amp;amp;face=0_0_800_276&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/PqSb8/hyPLdHbAY1/4dA6JL1acItnXkWTUOUws1/img.png?width=916&amp;amp;height=397&amp;amp;face=0_0_916_397,https://scrap.kakaocdn.net/dn/u1J4n/hyPJ3e7K9A/gfVuKGzLL585r4xq4k3wK0/img.png?width=800&amp;amp;height=341&amp;amp;face=0_0_800_341,https://scrap.kakaocdn.net/dn/dl3zT4/hyPLee1bcu/1ie93Td6lVk9OKadKxmhQ1/img.png?width=800&amp;amp;height=276&amp;amp;face=0_0_800_276');&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;Data Access&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Data Access Object (DAO) support in Spring is aimed at making it easy to work with data access technologies (such as JDBC, Hibernate, or JPA) in a consistent way. This lets you switch between the aforementioned persistence technologies fairly easily, a&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <category>AOP</category>
      <category>cglib</category>
      <category>jdk</category>
      <category>주의사항</category>
      <category>트랜잭션</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/140</guid>
      <comments>https://giron.tistory.com/140#entry140comment</comments>
      <pubDate>Sun, 11 Sep 2022 15:04:19 +0900</pubDate>
    </item>
    <item>
      <title>HTTP1.0 과 HTTP1.1 과 HTTP2 그리고 QUIC</title>
      <link>https://giron.tistory.com/139</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP 1.0&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 HTTP 1.0은 TCP Connection당 하나의 URL만 fetch하며, 매번 request/response가 끝나면 연결이 끊기므로 필요할 때마다 다시 연결해야하는 단점이 있어 속도가 현저히 느리다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 새로운 연결로 성능 저하&lt;/li&gt;
&lt;li&gt;서버 부하 비용 증가&lt;/li&gt;
&lt;li&gt;GET, HEAD, POST의 method가 사용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HEAD는 Header의 정보만 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP 1.1&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;persistent Connection(keep-alive)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지정한 timeout 동안 커넥션을 닫지 않는다.&lt;/li&gt;
&lt;li&gt;기본적으로 keep-alive이고 사용하지 않을 때만 헤더에 ~을추가하여 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;캐시를 두어 성능을 향상시켰고 데이터를 압축해서 보낸다.&lt;/li&gt;
&lt;li&gt;OPTION, PUT, DELETE, TRACE의 method를 사용한다.&lt;/li&gt;
&lt;li&gt;multiple request에 대한 처리가 가능하고 request/response가 파이프라인 방식으로 진행이 가능하다.&lt;/li&gt;
&lt;li&gt;호스트 헤더를 통해서 Virtual Hosting 이 가능해졌다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 ip로 여러 도메인을 운영할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;keep-alive&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blEIcz/btrLtYv0djt/E1KqAcaUjrWQI4rEvCnhfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blEIcz/btrLtYv0djt/E1KqAcaUjrWQI4rEvCnhfK/img.png&quot; data-alt=&quot;keep-alive&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blEIcz/btrLtYv0djt/E1KqAcaUjrWQI4rEvCnhfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblEIcz%2FbtrLtYv0djt%2FE1KqAcaUjrWQI4rEvCnhfK%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;1082&quot; height=&quot;1004&quot; data-filename=&quot;Untitled (1).png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;keep-alive&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파이프라이닝&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 하나의 커넥션에서 응답을 기다리지 않고 순차적인 여러 요청을 연속적으로 보내 그 순서에 맞춰 응답하여 지연 시간을 줄인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wyBLW/btrLqd8nQ8z/MgXncq51KNYXzKHkfx6vF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wyBLW/btrLqd8nQ8z/MgXncq51KNYXzKHkfx6vF1/img.png&quot; data-alt=&quot;파이프라이닝&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wyBLW/btrLqd8nQ8z/MgXncq51KNYXzKHkfx6vF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwyBLW%2FbtrLqd8nQ8z%2FMgXncq51KNYXzKHkfx6vF1%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;538&quot; height=&quot;388&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파이프라이닝&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-하지만 Head of Line Blocking 처럼 1, 2, 3번 순서로 올 때, 1번에서 문제가 발생해 blocking이 생기면 3 요청이 모두 안 가게 되는 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Header 구조의 중복 request의 헤더에 중복된 값이 들어갈 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;호스트 헤더&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 1.0 환경에서는 하나의 IP에 여러 개의 도메인을 운영할 수 없었지만 HTTP 1.1 부터 가능하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ HTTP 1.1 에서 Host 헤더의 추가를 통해 비로소 Virtual Hosting이 가능해졌다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP 2.0&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RNdY7/btrLu2dqhKL/ygjXOy3apNt6dgkenSEkN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RNdY7/btrLu2dqhKL/ygjXOy3apNt6dgkenSEkN0/img.png&quot; data-alt=&quot;HTTP 2.0&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RNdY7/btrLu2dqhKL/ygjXOy3apNt6dgkenSEkN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRNdY7%2FbtrLu2dqhKL%2FygjXOy3apNt6dgkenSEkN0%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;660&quot; height=&quot;350&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HTTP 2.0&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 1.X는 평문을 사용했지만 2.0부터는 바이너리 포맷으로 인코딩된 Message, Frame 으로 구성된다. 즉, 바이너리로 전달한다!!&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;Multiplexed Streams: 요청과 응답에 멀티플렉싱이 가능하다. - 바이너리 프레임으로 나누고 잔송받은 쪽에서 다시 조립한다.( 받는 쪽에서 다시 조립하므로 요청과 응답의 순서가 중요하지 않으므로 HOLB해결)&lt;/li&gt;
&lt;li&gt;Stream Prioritization - 리소스간 우선순위 설정 가능&lt;/li&gt;
&lt;li&gt;Server push - 클라이언트가 요청하지 않는 리소스도 push 가능하다.&lt;/li&gt;
&lt;li&gt;Header Compression - 헤더의 크기를 줄인다.(중복 제거) 약 85%&lt;/li&gt;
&lt;li&gt;HTTP1.X가 결국 지연시켜서 한 번에 요청/응답을 처리하는 방식에서 2.0부터는 각 응답을 멀티플렉싱 즉, frame으로 나누어서 병렬로 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HOLB(HeadOfLineBlocking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP에서 HOLB 은 처리가 가능했다. 바이너리 프레임으로 전달하므로 병렬적으로 응답받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP에서 HOLB 가 HTTP2에서도 문제이다. 패킷이 순서에 맞춰서 가야하기 때문에 앞에가 에러나면 뒤에 페킷도 기다려야한다. 따라서 때에따라 HTTP1.1보다 느릴수있다. 예를 들어,&amp;nbsp;HTTP/2로 다중화된 요청은&amp;nbsp;TCP에서는 단순한 패킷이므로 패킷이 막히면 전체가 지연되는 문제는 피할 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TCP통신의 문제 HOLB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2는 TCP를 사용하며 이전 HTTP 버전을 사용할 때 보다 더 적은 TCP 연결을 사용한다. TCP는 신뢰할 수 있는 전송 프로토콜이고 기본적으로 두 머신 간의 가상 체인으로 생각해도 된다. 네트워크의 한쪽 끝에 넣은 것이 최종적으로 다른 쪽 끝에 같은 순서로 나올 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqHUME/btrLu8YY2Ka/JkgPyQ3RsarKBaCx8Qr1oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqHUME/btrLu8YY2Ka/JkgPyQ3RsarKBaCx8Qr1oK/img.png&quot; data-alt=&quot;stream&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqHUME/btrLu8YY2Ka/JkgPyQ3RsarKBaCx8Qr1oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqHUME%2FbtrLu8YY2Ka%2FJkgPyQ3RsarKBaCx8Qr1oK%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;781&quot; height=&quot;193&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;stream&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;QUIC &amp;amp; HTTP3 - UDP로 만들어짐(구글이 만듦) - 전송프로토콜&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (4).png&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KQuH3/btrLotDAwEo/kc6nkEzUzw7sLHthLnekH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KQuH3/btrLotDAwEo/kc6nkEzUzw7sLHthLnekH0/img.png&quot; data-alt=&quot;TCP header&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KQuH3/btrLotDAwEo/kc6nkEzUzw7sLHthLnekH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKQuH3%2FbtrLotDAwEo%2Fkc6nkEzUzw7sLHthLnekH0%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;620&quot; height=&quot;271&quot; data-filename=&quot;Untitled (4).png&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TCP header&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (5).png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgE09f/btrLqelQjpp/KuEmoCK3TTw5RMA7lQZP7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgE09f/btrLqelQjpp/KuEmoCK3TTw5RMA7lQZP7K/img.png&quot; data-alt=&quot;UDP header&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgE09f/btrLqelQjpp/KuEmoCK3TTw5RMA7lQZP7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgE09f%2FbtrLqelQjpp%2FKuEmoCK3TTw5RMA7lQZP7K%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;719&quot; height=&quot;110&quot; data-filename=&quot;Untitled (5).png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;UDP header&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빠르기도 중요하지만 신뢰성도 중요할 것 같은데 왜 UDP로 전송할까?&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tcp는 신뢰성을 확보하지만 지연을 줄이기 힘든 구조 - 패킷 구조상 넣을 게 많다.
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반면에 UDP는 패킷 구조가 작다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;핸드쉐이크 없이 바로 데이터 주고받고 연결 성공시 설정을 캐싱한다.
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;connection UUID를 식별자로하여 통신하므로 커넥션 재수립 필요 X없으므로 가능하다&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;TLS 기본적으로 사용한다. IP SPoofing, 재연공격 방어&lt;/li&gt;
&lt;li&gt;독립된 스트림으로 하나가 끊겨도 다른 하나가 작동하여 병렬 처리한다.&lt;/li&gt;
&lt;li&gt;HTTP3&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HTTP 1.0&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;connection하나당 하나의 요청/응답만 받는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HTTP 1.1&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;keep-alive&lt;/b&gt;와 &lt;b&gt;파이프라이닝&lt;/b&gt;을 통해 해당 문제를 해결한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시&lt;/b&gt;를 두어 성능을 향상시킨다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;OPTION, PUT, DELETE 등 다양한 메서드도 지원한다.&lt;/li&gt;
&lt;li&gt;host header를 통해 virtual hosting이 가능해졌다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HOLB로 앞의 패킷이 안가면 뒤의 패킷들도 전송이 지연된다.&lt;/li&gt;
&lt;li&gt;파이프라이닝을 통해 사용하지만 헤더의 중복이 발생한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HTTP 2.0&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;바이너리 포맷으로 인코딩된 Message, Frame 으로 구성된다.&lt;/li&gt;
&lt;li&gt;패킷을 쪼개어 보내므로 HOLB가 해결이 된다.&lt;/li&gt;
&lt;li&gt;헤더의 중복을 해결했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;여전히 TCP기반 통신이라서 데이터 프레임이 도착하지 않으면 지연이 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;QUIC&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UDP로 통신해서 빠르다. - 패킷의 구조가 작으므로&lt;/li&gt;
&lt;li&gt;TLS를 기본적으로 사용한다.&lt;/li&gt;
&lt;li&gt;핸드쉐이크 없이 통신한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx에서 http2 적용하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;23&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PJ7wB/btrLvJklta8/XHyWERr93LbPb6koyYYSq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PJ7wB/btrLvJklta8/XHyWERr93LbPb6koyYYSq1/img.png&quot; data-alt=&quot;기존의 http1.1인 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PJ7wB/btrLvJklta8/XHyWERr93LbPb6koyYYSq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPJ7wB%2FbtrLvJklta8%2FXHyWERr93LbPb6koyYYSq1%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;210&quot; height=&quot;23&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;23&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존의 http1.1인 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1662597921709&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    # ssl설정을 해주고, http2를 적어주면 쉽게 설정할 수 있다.
    listen 443 ssl http2;
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 끝이다. 이후, sudo service nginx reload를 하면 아래처럼 http2가 적용되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;288&quot; data-origin-height=&quot;26&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bty7vr/btrLFIzjl9B/csbskEI7BqrdRuZhlqk0Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bty7vr/btrLFIzjl9B/csbskEI7BqrdRuZhlqk0Zk/img.png&quot; data-alt=&quot;적용후 http2.0&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bty7vr/btrLFIzjl9B/csbskEI7BqrdRuZhlqk0Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbty7vr%2FbtrLFIzjl9B%2FcsbskEI7BqrdRuZhlqk0Zk%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;288&quot; height=&quot;26&quot; data-origin-width=&quot;288&quot; data-origin-height=&quot;26&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;적용후 http2.0&lt;/figcaption&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2021-09-20-http2/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tecoble.techcourse.co.kr/post/2021-09-20-http2/&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>HTPP1.1</category>
      <category>HTTP</category>
      <category>HTTP1.0</category>
      <category>HTTP2.0</category>
      <category>HTTP3.0</category>
      <category>QUIC</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/139</guid>
      <comments>https://giron.tistory.com/139#entry139comment</comments>
      <pubDate>Thu, 8 Sep 2022 09:51:06 +0900</pubDate>
    </item>
    <item>
      <title>jwt토큰 디코딩하면 페이로드 값을 다 아는데 왜 서명을 하는걸까?</title>
      <link>https://giron.tistory.com/138</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;jwt 토큰이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT(JSON Web Token)는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 토큰이다. 이 정보는 &lt;b&gt;디지털 서명되어 있으므로 확인하고 신뢰&lt;/b&gt;할 수 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JWT는 HMAC 알고리즘을 사용하거나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;RSA&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ECDSA&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하는 공개/개인 키 쌍을 사용하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;서명할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;한가지 오해&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 jwt를 공부할 때, 어차피 디코딩하면 헤더에서 토큰 타입이나 알고리즘을 알 수있는데 왜 서명을 하는가였다. 그이유를 설명하자면 jwt의 서명의 목적을 생각해야한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디지털 서명되어 신뢰할 수 있는 이유는?(서명하는 이유)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;b&gt;JWT 토큰은 자체적으로 토큰 유효성 검사가 가능하다&lt;/b&gt;! 즉, HMAC또는 RSA와 같은 공개 또는 비대칭키를 통해 서명하기 때문이다. 단순히 JWT토큰을 받아서 해당 토큰이 위변조되었는지만 확인하여 &lt;b&gt;위변조가 되지 않으면 JWT토큰 내부에서 사용자 정보를 꺼내 사용할 수 있기 때문이다.&lt;/b&gt; 바로 이렇게 토큰의 위변조가 있었는지 혹은 위변조를 방지하는 기법 중 하나를 HMAC(Hash-based Message Authentication)이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HMAC 알고리즘이란&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fefefe; color: #000000;&quot;&gt;MAC(Message Authenticate Code)?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fefefe;&quot;&gt;원본 메시지와 전달된 메시지를 비교하여 &lt;b&gt;변조 여부&lt;/b&gt;를 확인하는 방식이 MAC이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fefefe; color: #000000;&quot;&gt;이때 Hash와 MAC을 사용하여 변조 여부를 확인하는 방식이 HMAC이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fefefe; color: #000000;&quot;&gt;MD5나 SHA1, SHA256 같은 암호화 해시 함수를 사용하여 원본 메시지를 암호화한다. 이때, 일반 해싱 알고리즘과의 차이점은 HMAC은 &lt;b&gt;송수진자가&lt;/b&gt; 미리 &lt;b&gt;해시 암호 키를 나눠 가지고 있다&lt;/b&gt;는 것이다. 즉, 대칭키 알고리즘 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQhMCd/btrKXFYBPuG/LK6oLskVJmWygqbwmBeQM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQhMCd/btrKXFYBPuG/LK6oLskVJmWygqbwmBeQM0/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQhMCd/btrKXFYBPuG/LK6oLskVJmWygqbwmBeQM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQhMCd%2FbtrKXFYBPuG%2FLK6oLskVJmWygqbwmBeQM0%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;784&quot; height=&quot;422&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사전에 Sender와 Receiver는 사용할 공유 키(Secret key)를 공유한다. 그리고, 양쪽에서 사용할 해시 알고리즘을 정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Sender는 공유키를 사용해서 UserId를 해시한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Sender는 원본 UserId와 그 해시결과(HMAC)을 쿼리스트링 값으로 Receiver에게 전달한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Receiver는 받은 UserId를 공유키를 사용하여 같은 알고리즘으로 해시한 결과(Receiver's HMAC)를 만든다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Receiver가 만든 HMAC과 쿼리스트링으로 받은 HMAC이 같다면 UserId는 변경되지 않았다고 신뢰할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HMAC 동작방식&lt;/h4&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;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yD0LX/btrKVxGx4N8/KPyJxGGY2k9ri7pDzAwi3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yD0LX/btrKVxGx4N8/KPyJxGGY2k9ri7pDzAwi3K/img.png&quot; data-alt=&quot;수도코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yD0LX/btrKVxGx4N8/KPyJxGGY2k9ri7pDzAwi3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyD0LX%2FbtrKVxGx4N8%2FKPyJxGGY2k9ri7pDzAwi3K%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;701&quot; height=&quot;592&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수도코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 id=&quot;RESTAPI제작시꼭알고있어야할HMAC기반인증-secretkey길이검사&quot; data-ke-size=&quot;size20&quot;&gt;1. secret key 길이 검사&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력받은 key 길이가 블록 사이즈보다 클 경우 hash 함수에 key 를 넣어서 키를 생성합니다.&lt;/li&gt;
&lt;li&gt;key 길이가 블록 사이즈보다 작을 경우 적은 길이만큼 '\0' 을 패딩해서 오른쪽을 채웁니다. 즉 key 길이가 40 이고 block size 가 64 일 경우 key 의 41번째 바이트부터 '0' 을 24개를 padding 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;RESTAPI제작시꼭알고있어야할HMAC기반인증-paddedkey구현&quot; data-ke-size=&quot;size20&quot;&gt;2. padded key 구현&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;i_key_pad 값을 구하기 위해서 key 와 0x36 을 XOR 연산합니다.&lt;/li&gt;
&lt;li&gt;o_key_pad 값을 구하기 위해서 key 와 0x5c 를 XOR 연산합니다.&lt;/li&gt;
&lt;li&gt;이제 구한&amp;nbsp;i_key_pad&lt;span&gt;&amp;nbsp;&lt;/span&gt;값에 전송할 message 를 concatenation 한 후에 hash 를 돌리고 나온 값을&amp;nbsp;o_key_pad&amp;nbsp;에&amp;nbsp; concatenation 한후에 다시 hash 를 한 값이 HMAC 이 됩니다.&lt;/li&gt;
&lt;li data-hasbody=&quot;true&quot; data-macro-name=&quot;sp-macrooverride-plaintextbody-block&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;lisp&quot; data-bidi-marker=&quot;true&quot;&gt;&lt;code&gt;hash(o_key_pad ∥ hash(i_key_pad ∥ message))&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;이제 송신자에게 HMAC 값과 message 를 서버에 전송하면 서버가 보유한 secret key 를 사용해서 전송받은 message 에서 HMAC 을 계산하고 client 가 전송한 HMAC 과 비교하면 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;*재연 공격(Reply attack)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서명처럼 무결성이 있는 암호화된 패킷을 공격하는 방법으로 패킷을 잡아두었다고 계속 그 패킷을 보내는 재연 공격이 있다. 이러한 재연 공격에 대해서는 난수 또는 TimeStamp로 감싸서 보내면 수신자가 받았을 때, 옛날 시간이면 재연공격으로 판단해서 방어하는 방법이 있다. 이러한 방법은 추후 OTP원리에도 적용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 흔히들 알고있어서 넘어가려고 했지만 조금만 언급하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 header, payload, signiture로 이루어져 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;header&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;token type과 서명 알고리즘(HMAC, RSA..)이 들어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 RSA는 타원곡선함수를 사용한 비대칭키 암호화로 키가 길어서 느리다는 단점이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1661865539862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;payload&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;토큰에 담을 Claim 정보를 포함하고 있으며, Payload에 담는 정보의 한 조각을 Claim이라 부르고 이는 key/value pair로 이뤄져 있다. &lt;b&gt;여러 조각(claim)을 넣을 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;등록된 클레임
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;iss: 토큰 발급자(issuer)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;sub: 토큰 제목(subject)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;exp: 토큰 만료 시간(expiration), NumericData 형식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;와 같은 정해진 형식으로 데이터를 담는다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공개 클레임&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공개용 정보를 위해 사용된다. 충돌 방지를 위해 URI 포맷을 사용한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비공개 클레임&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버와 클라이언트 사이의 임의로 지정한 정보를 저장한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;signiture&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인코딩된 헤더와 인코딩된 페이로드 그리고 헤더에 지정된 알고리즘을 가져와 아래와같은 형식으로 만듭니다.(아래는 HMAC예시입니다.) 개인키로 서명된 토큰은 발급자도 확인할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661868487814&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT 토큰 주의점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 헤더를 통해 JWT 토큰을 보내는 경우 토큰이 너무 커지지 않도록 해야 합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;일부 서버는 8KB 이상의 헤더를 허용하지 않습니다. 따라서 토큰에 너무 많은 정보를 포함하려고 하면 안된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JWT는 언제 사용할까?&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;권한 부여
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유저가 로그인할 때, jwt에 권한을 담아서 전달하면 유저는 어느 부분까지 접근이 가능한지에 대한 권한을 가질 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;정보 전달
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jwt는 헤더와 페이로가 서명이 된 토큰이므로 정보의 신뢰성이 있다. 또한 개인키로 암호화한 jwt는 송신자가 누구인지 알 수도 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSON 웹 토큰을 사용해야 하는 이유는 무엇입니까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SWT(Simple Web Tokens)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;및&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;SAML(Security Assertion Markup Language Tokens&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;)과 비교할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JWT(JSON Web Tokens)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 이점에 대해 이야기해 보겠다. 참고로 swt는 xml기반의 토큰이다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON은 XML보다 덜 장황하기 때문에 인코딩될 때 크기도 작아져 JWT가 SAML보다 더 간결해집니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;따라서 JWT는&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;XML 기반의 SAML 방식보다 크기가 작습니다. 또한 SWT방식은 HMAC알고리즘만 적용이 가능하다. JWT는 개인/공개키가 가능하므로 정보전달 할 때, 송신자 개인키로 암호화할 시에 송신자가 누구인지 알아 신뢰성 높게 사용할 수 있다. 또는 어떤 알고리즘을 사용했는지 범위가 넓어지므로 공격자가 공격하기 더 어렵게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. jwt를 사용하면 인코딩 됐을 때, 다른 토큰보다 덜 장황해서 크기가 작다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. HMAC뿐만 아니라 공개/개인키로 서명이 가능해서 공격자가 어떻게 서명했는지 알아차리기 더 어렵다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>HMAC</category>
      <category>jwt토큰</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/138</guid>
      <comments>https://giron.tistory.com/138#entry138comment</comments>
      <pubDate>Thu, 1 Sep 2022 01:11:22 +0900</pubDate>
    </item>
    <item>
      <title>토큰과 세션(3) - 선택</title>
      <link>https://giron.tistory.com/137</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 저희 &lt;b&gt;공식&lt;/b&gt;팀의 서비스는 지식 공유 게시판(약간 stackoverflow느낌)의 웹 플랫폼입니다. 저희 서비스에서는 세션과 토큰 중 어떤 게 적합한 인증방식일지 고민해봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;일반적인 오해&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적으로 &lt;b&gt;웹사이트는 쿠키를 사용한 세션 인증&lt;/b&gt;을 사용하고 &lt;b&gt;모바일 앱/SPA는 Authorization 헤더와 함께 토큰 인증&lt;/b&gt;을 사용하지만 반드시 그런 것은 아니다. Authorization Header와 Cookies는 전송 메커니즘에 관한 것입니다. 토큰과 세션은 기본적으로 서버 측이든 클라이언트 측이든 권한 부여 상태가 처리되는 위치에 관한 것입니다. 예를 들어, 서버는 쿠키를 통해 JWT 토큰을 발행하거나 &quot;Authorization&quot; 헤더에 상태 저장 세션 ID가 제공될 것으로 예상할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://www.authgear.com/post/session-vs-token-authentication&quot;&gt;https://www.authgear.com/post/session-vs-token-authentication&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그럼 언제 토큰, 언제 세션을 사용하는 게 좋을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 웹서비스에서는 ID/PW로 인증을 확인받고 세션을 유지하는 방식을 취해왔습니다. 이 방식은 &lt;b&gt;쿠키를 날린다거나 세션 유효기간이 만료되면 재인증&lt;/b&gt;해야 하는 번거로움이 있습니다.물론 이런 번거로움이 나쁘다는 것이 아니라, 유저의 사용환경상 그러는 편이 나은 선택일 것입니다. &lt;b&gt;웹은 어디서건 접속할 수 있고, 때문에 본인만 사용하는 PC라는 것을 확인할 수 없기 때문에&lt;/b&gt; 적절한 시점에 로그아웃을 해줘야 보안상 안전할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;b&gt;앱 사용환경은 조금 다릅니다&lt;/b&gt;. &lt;b&gt;앱&lt;/b&gt;은 스마트폰을 통해 설치하며, 스마트폰은 &lt;b&gt;&amp;lsquo;자신의 것&amp;rsquo;이라는 것에 컨센서스가 있기 때문입니다.&lt;/b&gt; 따라서 앱-서버간 인증에서는 매번 로그아웃을 하는 것은 일부상황(보안수준이 매우 높아야 하는 금융 혹은 private 서비스)을 제외한 유저에게 더욱 번거로운 경험이라고 생각됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://techblog.woowahan.com/2617/&quot;&gt;https://techblog.woowahan.com/2617/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wb3gk/btrKLg4Ipci/8j25HYL4BMBkrTICv9Ag9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wb3gk/btrKLg4Ipci/8j25HYL4BMBkrTICv9Ag9K/img.png&quot; data-alt=&quot;차이점&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wb3gk/btrKLg4Ipci/8j25HYL4BMBkrTICv9Ag9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwb3gk%2FbtrKLg4Ipci%2F8j25HYL4BMBkrTICv9Ag9K%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;959&quot; height=&quot;638&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;차이점&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 차이점은 세션 기반 인증은 서버가 세부 사항을 저장하고, 반면에 토큰 기반은 클라이언트가 토큰을 가지고 있는 형태이다. 두 방법 모두 취약점이 있고, 자신의 요구와 응용 프로그램에 더 적합한 방법을 결정해야 한다. 토큰은 대역폭이 크기 때문에 저장해서 사용하는 것보단 대역폭이 작은 세션을 사용하는 게 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*네트워크 성능에 미치는 요소 - 대역폭(bandWidth), 소요 시간(Latency)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소요시간 = 전파시간 + 전송시간 + 큐잉시간&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전파시간 = 거리 / 광속&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송시간 = 크기 / 대역폭&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생각 정리&lt;/h3&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;b&gt;토큰같은 경우&lt;/b&gt;는 탈취를 당하면 &lt;b&gt;강제로 로그아웃 시키&lt;/b&gt;거나 &lt;b&gt;만료시키는 방법이 없기 때문이다&lt;/b&gt;. 따라서 토큰을 사용한다면 &lt;b&gt;애초에 탈취당하지 않도록 하는 방법&lt;/b&gt;을 강구해야 할 것 같다. 리프레시토큰의 재발급 요청 메서드를 엑세스토큰이 만료되었을 때만 올 수 있도록 처리하는 방법도 있을 것 같다. 혹은 엑세스토큰을 함께 요청으로 받고, &lt;b&gt;엑세스토큰의 만료일과 payload를 확인하는 방식으로 처리할 수 있지 않을까 생각이 든다! (payload의 값은 유효하지만 만료만 된 경우는 탈취당했다고보기 어렵다는 판단하에..!)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 &lt;b&gt;세션&lt;/b&gt;을 사용한다면 그만큼 보틀넥이되므로 확장에 대한 단점은 무시할 순 없다. 하지만 세션은 스토리지에 정보가 저장되므로 탈취가 당했다고 인지하면( ex) 접속 ip주소가 바뀌었다던지) 세션을 날려서 재로그인 또는 비밀번호 변경을 하도록 요청할 수 있다. 보틀넥 현상도 스토리지를 이중화하면 어느 정도는 해결되지 않을까 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 토큰같은 경우는 확장성이 좋고, 트래픽이 몰릴 경우가 빈번한 서비스에서 사용하는 것이 좋다고 생각한다. 사내 내부 서비스에서 검증을 할 때는 상대적으로 토큰이 외부에 노출될 경우가 적기 때문에 굳이 세션스토리지를 갖지 않아도 되는 토큰 방식이 좋아보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;b&gt;배달의 민족&lt;/b&gt;처럼 이벤트로 트래픽이 몰릴 가능성이 있는 경우 토큰을 사용하는 편이 좋다고 생각한다. 또한 앱이라는 점도 다른 외부 PC혹은 모바일에서 본인의 계정에 접속할 일은 적기 때문에 강제 로그아웃이 크게 필요하지 않는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 &lt;b&gt;네이버&lt;/b&gt;같은 경우는 트래픽이 갑자기 몰릴 경우가 적고 본인만 사용하는 pc가 아니기 때문에 적절한 때에 강제 로그아웃이 필요하다. 따라서 세션을 사용해도 나쁘지 않은 선택이라고 생각한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022-08-30 좋은 글을 찾아 소개하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/boojongmin/memo/issues/7&quot;&gt;https://github.com/boojongmin/memo/issues/7&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거의 한 회사에서는 browser용 API app용 API를 따로 제공했다.왜냐하면 browser는 로그인이후 session을 이용했고 app은 로그인후 엑세스 토큰으로 통신을 하였기 때문이다.&lt;br /&gt;이때 이걸 선택한 이유는 2가지로 생각이 된다.&lt;br /&gt;1. app은 cookiejar 기능이 없기 때문에 브라우저랑 다르게 동작해야한다.(사실 cookiejar 구현체를 쓰면 쉽긴한데...암튼...)&lt;br /&gt;2. app은 http 호출하여 json 형태로 응답하고 웹은 서버에서 http 호출시 html 형태로 응답하기 때문에 response content/type 미스매칭&lt;br /&gt;사실 이때는 나도 이렇게 생각할 수 밖에 없었던거 같다그래서 앱/웹 두벌을 만들기 인력/시간이 부족한 스타트업은 웹뷰를 이용한 앱을 많이 선택할 수 밖에 없었던게 당연한 흐름이였던거 같다.&lt;br /&gt;하지만 몇년전 한회사에서 나는사용자에게 제공할 앱은 flutter를 이용하여 ios/android를 개발하고운영툴은 웹으로 vuejs로 개발을했었다.&lt;br /&gt;나는 vuejs는 할 수 있으니 spring을 이용하여 api를 만들고 vuejs로 화면을 그리고 프론트와 백엔드간 api를 통해 데이터를 주고 받고 개발을 하였다.&lt;br /&gt;이때 웹은 JWT를 이용하였는데 이때 느낀 신선함은 flutter 앱개발자에게도 동일한 api를 제공해도 된다는 점이였다.&lt;br /&gt;즉 웹/앱 구별하여 api를 두벌 뽑을 필요가 없었다.&lt;br /&gt;기술이 모던해지면서 vue나 reactjs와 같은 기술이 나왔고 이 Single Page Application(SPA)를 통해 web도 이제는 app과 비슷해졌고 이로 인해 백엔드 개발자는 좀 더 api 개발시에 효율을 느낄 수 있었다.&lt;br /&gt;즉 나는 session 베이스는 response content type이 html일 때 자연스럽고response type이 application/json일때는 JWT을 더 선호한다는 것이다.(무조건 이렇게 해야한다고 할 수 없지만 내 기준은 이렇다)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추가 2022-09-06&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;s&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;(이 질문을 할때 떠올랐다 ㅋㅋ)&lt;/span&gt;&lt;/s&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 리프레시 토큰을 저장하는 경우는 엑세스 토큰이 만료될 때, 요청이 온다. 따라서 세션을 사용하면 매번 요청이 올때마다 저장소를 찌르지만 토큰을 저장할 경우 엑세스 토큰이 만료될 때만 찌르기 때문에 같이 저장을 한다고 하더라도 이득이 있을 것같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;즉, &lt;b&gt;리프레시 토큰을 저장하고 리프레시 토큰을 검증할 때, 엑세스토큰과 리프레시 토큰을 함께 보내서 -&amp;gt; 액세스 토큰이 유효한데 만료만 되었다면 리프레시 토큰과 함께 발급해주는 방향이 최선이라고 생각이 된다!!!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&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;a href=&quot;https://www.baeldung.com/cs/tokens-vs-sessions&quot;&gt;https://www.baeldung.com/cs/tokens-vs-sessions&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/cd/E19462-01/819-4669/adrbc/index.html&quot;&gt;https://docs.oracle.com/cd/E19462-01/819-4669/adrbc/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.interviewarea.com/faq/does-google-use-sessions-or-jwt&quot;&gt;https://www.interviewarea.com/faq/does-google-use-sessions-or-jwt&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>세션</category>
      <category>토큰</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/137</guid>
      <comments>https://giron.tistory.com/137#entry137comment</comments>
      <pubDate>Mon, 29 Aug 2022 16:00:37 +0900</pubDate>
    </item>
    <item>
      <title>토큰과 세션(2) - 세션기반 인증 확장 방법</title>
      <link>https://giron.tistory.com/136</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 확장되어 여러 개를 갖게 된다면 요청이 들어올 때마다 로드밸런싱을 통해 부하를 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 세션을 사용하면 A라는 서버로 처음에 로그인을 해서 세션 ID가 만들어지고 로그인을 성공한다. 하지만 사용자가 다시 요청을 보낼 때 이번엔 로드밸런싱에 의해 B 서버로 갔다면 &lt;b&gt;B서버에는 사용자의 세션ID가 없으므로&amp;nbsp;&lt;/b&gt;다시 로그인을 시도하라는 요청이 올 수 있다. 이러한 &lt;b&gt;세션과 로드밸런싱의 문제를 해결하기 위해 sticky session이 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sticky session&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 로그인했을 때, 갔던 서버에서 세션 ID를 만들고 로그인이 성공하면 그 회원의 요청은 해당 서버로만 가도록 하는 것이다. 즉, 세션이 만들어진 서버로만 요청이 가는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방식은 cookie를 사용하는 방식으로 구현할 수 있다. 정합성에서는 좋지만 단점이 있다.&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 단점들을 해결하기 위해 Session Clustering이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Session clustering&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;여&lt;span style=&quot;background-color: #ffffff;&quot;&gt;러 대의 컴퓨터들이 하나의 시스템처럼 동작하도록 만드는 것을 클러스터링이라고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TbfeX/btrKHVzHz4N/7mcsWEbCWqUKJsIt2iQYpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TbfeX/btrKHVzHz4N/7mcsWEbCWqUKJsIt2iQYpK/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TbfeX/btrKHVzHz4N/7mcsWEbCWqUKJsIt2iQYpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTbfeX%2FbtrKHVzHz4N%2F7mcsWEbCWqUKJsIt2iQYpK%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;1017&quot; height=&quot;519&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;519&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림처럼 요청이 들어오면 디비에 세션을 저장한다. 그 후, 저장된 세션을 각 was의 디비에 똑같이 복제를 한다. 이렇게 하면 정합성도 해결되고, WAS1이 죽는다 하더라도 다른 WAS를 이용해도 되므로 사용자 입장에서는 로그아웃이 되는 불편함을 겪을 일이 없다. 하지만 이러한 세션 클러스터링의 단점은 보기에도 느끼겠지만 굉장히 수정이 빈번하다. 이는 휴먼 에러를 초래할 수 있다고 생각된다.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 was가 중복된 값을 가지므로 메모리를 차지하는 용량이 크다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;새로운 서버가 하나 뜰 때마다 기존의 was에서 새로운 서버의 IP/Port를 입력해서 클러스터링 해줘야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;세션을 각 서버에 전달, 저장해야하기 때문에 서버 수에 비례하여 네트워크 트래픽이 증가하게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;세션을 복제하여 전달 중에 전달되지 않는 곳으로 요청이 들어오면 로그인에 실패한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #494e52;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Session server분리&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #494e52;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Redis를 이용한 Session clustreing을 이용하면 위의 상황을 피할 수 있다. redis뿐만 아니라 세션 스토리지를 따로 두어 관리할 수도 있는데 redis만의 장점이 있기 때문에 많이들 사용한다고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vR2Y3/btrKGOg1Wfj/dkrjbWYUr6sDO1NGILWzg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vR2Y3/btrKGOg1Wfj/dkrjbWYUr6sDO1NGILWzg0/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vR2Y3/btrKGOg1Wfj/dkrjbWYUr6sDO1NGILWzg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvR2Y3%2FbtrKGOg1Wfj%2FdkrjbWYUr6sDO1NGILWzg0%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;682&quot; height=&quot;481&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그림과 같이 별도의 서버를 구성한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;was가 추가된다 하더라도 복제할 세션이 추가되지 않으므로 메모리의 낭비가 줄어든다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존의 was는 건드리지 않아도 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;redis를 주로 사용하는 이유로는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적인 disk보다 redis는 in-memory db이기 때문에 비용이 큰 I/O에 대한 성능이 월등하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;disk에 저장하면 스케줄러등을 사용하여 주기적으로 만료된 토큰을 만료 처리하거나 제거해야 한다. 하지만, redis는 기본적으로 데이터의 유효기간(time to live)을 지정할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 그림처럼 저장 공간이 한 곳에서 관리하므로 만약 스토리지가 날라간다면 모든 회원이 로그아웃 될 것이다. 또한 결국 모든 트래픽이 한 redis에서(replication을 적용한다고 하더라도)&amp;nbsp; 받아내므로 보틀넥이 일어나지 않나라는 생각을 하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음번엔 토큰과 세션을 어떨 때에 사용해야 할지에 대해 정리를 해봐야겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최종 정리: &lt;a href=&quot;https://giron.tistory.com/137&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://giron.tistory.com/137&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #494e52;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Reference&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #494e52;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;a href=&quot;https://junshock5.tistory.com/84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://junshock5.tistory.com/84&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>ELB</category>
      <category>고정세션</category>
      <category>세션</category>
      <category>세션 클러스터링</category>
      <category>세션 확장</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/136</guid>
      <comments>https://giron.tistory.com/136#entry136comment</comments>
      <pubDate>Sat, 27 Aug 2022 19:06:32 +0900</pubDate>
    </item>
    <item>
      <title>토큰과 세션(1) - 토큰과 세션 그리고 쿠키에 대해서</title>
      <link>https://giron.tistory.com/135</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP의 특징&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;connectionless, stateless&lt;/h4&gt;
&lt;p id=&quot;stateless&quot; data-ke-size=&quot;size16&quot;&gt;connectionless는 클라이언트에서 http request를 서버에 보내면, 서버는 알맞은 http response를 보내고 접속을 끊는 특성이 있다. 왜냐하면 여러 클라이언트와 통신을 하려면 한 커넥션이 오래 물고 있으면 성능에 좋지 않기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 매번 연결을 끊고 다시 생성하는 비용도 만만치 않아서 (HTTP1.1에서) keep-alive를 통해서 timeout, max번 connection을 재활용하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stateless는 통신이 끝나면 상태를 유지하지 않는 특징이다. 로그인을 성공하고 다음에 글을 적으려고 하면 다시 인증을 해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 &lt;span style=&quot;color: #ececec;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;connectionless, stateless한 성격을 가진다. 이중 HTTP의 &lt;/span&gt;&lt;/span&gt;stateless는 어디에서 누가 요청을 줬는지 알 수가 없다. 하지만 쿠키를 사용한다면 이러한 stateless한 성격을 조금이나마 없앨 수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예시로는 로그인 시 &quot;아이디와 비밀번호를 저장하시겠습니까?&quot; 혹은 &quot;오늘 더 이상 이 창을 보지 않음&quot; 등이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿠키의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 만든 쿠키를 브라우저에게 보내고 브라우저는 해당 쿠키를 저장하고 모든 요청에 쿠키를 넣어서 보낸다.&lt;/li&gt;
&lt;li&gt;이러한 쿠키는 HTTP의 header영역을 통해서 송수신된다.&lt;/li&gt;
&lt;li&gt;쿠키는 단순히 &amp;lt;키&amp;gt;=&amp;lt;값&amp;gt;의 형태의 문자열이다.&lt;/li&gt;
&lt;li&gt;작은 저장 공간을 가진다. (4KB)&lt;/li&gt;
&lt;li&gt;만료 시점을 기준으로 두 가지로 나뉜다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;브라우저가 종료되면 삭제되는 &lt;b&gt;session cookies&lt;/b&gt;가 있다. 메모리에 저장되고 디스크에는 저장되지 않는다.&lt;/li&gt;
&lt;li&gt;유효 시간을 세팅하지 않는 쿠키를 &lt;span&gt;session cookie라고 한다. ex) 은행 사이트 같은 경우 다시 접속하면 끊기게&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;지정된 만료일에 삭제되는 &lt;b&gt;persistent cookies&lt;/b&gt;가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;300까지 저장이 가능하고, 하나의 도메인당 20개의 값만 가질 수 있다.&lt;/li&gt;
&lt;li&gt;웹 브라우저마다 쿠키에 대한 자원 형태가 다르므로 브라우저간의 공유가 불가능하다.&lt;/li&gt;
&lt;li&gt;보안에 취약하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메커니즘&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nF8zl/btrJEL0pBoP/xbKMGYlTVIfjKDfd9WkrOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nF8zl/btrJEL0pBoP/xbKMGYlTVIfjKDfd9WkrOk/img.png&quot; data-alt=&quot;매커니즘&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nF8zl/btrJEL0pBoP/xbKMGYlTVIfjKDfd9WkrOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnF8zl%2FbtrJEL0pBoP%2FxbKMGYlTVIfjKDfd9WkrOk%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;1029&quot; height=&quot;612&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;매커니즘&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버에서는 Set-Cookie라는 응답 헤더에 쿠키의 정보를 넣어서 보냅니다.&lt;/li&gt;
&lt;li&gt;서버로부터 쿠키를 받은&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;브라우저는 해당 쿠키를 클라이언트 컴퓨터의 하드디스크에 저장합니다. 그리고 브라우저가 동일한 서버에 요청을 할 때 저장해놓은 쿠키를&amp;nbsp;Cookie라는 요청 헤더에 실어서 돌려보냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 매번 요청이 일어날 때마다 쿠키를 자동으로 헤더에 담아서 보냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿠키는 웹 브라우저마다 저장되는 위치가 다릅니다. 하지만 모두 하드디스크에 저장이 되고 그러므로 브라우저를 껐다가 켜도 쿠키의 유효시간이 유효하다면 인증이 유지될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 3) 번처럼 쿠키의 특징은 &lt;b&gt;브라우저라는&lt;/b&gt; HTTP 클라이언트에서만 일어나는 특징입니다. 만약 `curl`과 같은 HTTP 클라이언트에서는 다른 헤더처럼 요청과 응답을 주고받아도 자동으로 값을 넣어서 요청이 보내지 진 않습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿠키 옵션&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;httpOnly는 javascript를 통한 xss공격을 막아주는 설정입니다.&lt;/li&gt;
&lt;li&gt;path는&lt;span&gt; '&lt;/span&gt;/' 하위 경로로 쿠키들이 오고 갈 수 있도록 해줍니다.&lt;/li&gt;
&lt;li&gt;secure는 https일 때만 쿠키를 주고받을 수 있도록 해줍니다. 따라서 man-in-the-middle-attack을 막아줍니다.&lt;/li&gt;
&lt;li&gt;sameSite는 같은 도메인에서만 쿠키를 주고받을 수 있도록 하여 csrf 공격을 막는데 도움을 줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*csrf공격:&lt;span style=&quot;color: #000000;&quot;&gt; 서비스의 기능 요청을 &lt;b&gt;크로스 도메인 환경&lt;/b&gt;, 즉 공격자가 구성한 웹 페이지를 이용하여 사용자의 인증정보를 포함해 전송하도록 만드는 공격. 일반적으로 &lt;b&gt;GET CSRF의 경우 img 태그&lt;/b&gt;, POST 등 다른 메소드를 이용한 CSRF는&lt;b&gt; form 태그&lt;/b&gt;가 많이 활용된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SameSite 와 CORS 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;origin&quot;은 훨씬 더 엄격합니다.&lt;span&gt; cors는 &lt;/span&gt;두 사이트 모두 동일한 스키마(HTTP/HTTPS), 포트 및 하위 도메인이 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 sameSite는 TLD+1 level까지만 일치하면 sameSite라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) &lt;a href=&quot;http://gongseek.site&quot;&gt;gongseek.site&lt;/a&gt; &lt;a href=&quot;http://www.gongseek.site&quot;&gt;www.gongseek.site&amp;nbsp;&lt;/a&gt; cors에 위반되지만 sameSite에는 위반되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SameSite&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;None: 아무런 제약이 없습니다. 보안에 취약하므로 cookie옵션에 secure를 설정하지 않으면 None으로 할 수 없습니다.&lt;/li&gt;
&lt;li&gt;Lax: 덜 엄격한 제한으로 Get의 경우에는 검증하지 않고 통과됩니다. 또한 &amp;lt;a 태그 또는 &amp;lt;form 태그가 와도 sameSite로 간주합니다.&lt;/li&gt;
&lt;li&gt;Strict: 엄격한 제한으로. sameSite일 때만 쿠키가 보내집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9v0rl/btrJSlMyDXT/XJqorS7tQKg2mcg9264tb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9v0rl/btrJSlMyDXT/XJqorS7tQKg2mcg9264tb1/img.png&quot; data-alt=&quot;세션 flow&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9v0rl/btrJSlMyDXT/XJqorS7tQKg2mcg9264tb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9v0rl%2FbtrJSlMyDXT%2FXJqorS7tQKg2mcg9264tb1%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;735&quot; height=&quot;358&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;세션 flow&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키가 클라이언트에서 관리가 되었다면 세션은 서버에서 관리한다. 크게 Client, Server, Session Storage로 나눠서 관리가 된다. 세션 객체는 Key에 해당하는 랜덤하게 생성된 SESSION ID와 value로 구성이 되어있다. value에는 &lt;b&gt;세션 생성 시간, 마지막 접근 시간, 사용자 IP등&lt;/b&gt;이 Map형태로 저장된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 메커니즘&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 로그인을 하면 세션 id가 만들어지고 세션 스토리지에 저장이 된다.&lt;/li&gt;
&lt;li&gt;서버에서는 브라우저의 쿠키에 SESSION ID를 저장한다.&lt;/li&gt;
&lt;li&gt;브라우저는 모든 요청에 대해 SESSION ID가 담겨있는 쿠키를 전달한다.&lt;/li&gt;
&lt;li&gt;서버는 쿠키에서 SESSION ID를 꺼내고, 세션 스토리지에서 꺼낸 유저의 SESSION ID와 비교하여 인증을 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stateful하며 서버에 정보를 저장하므로 보다 안전하다.&lt;/li&gt;
&lt;li&gt;각 클라이언트에게 고유 ID 부여한다.&lt;/li&gt;
&lt;li&gt;쿠키도 만료 시간이 있지만 &lt;b&gt;파일로 저장&lt;/b&gt;되기 때문에 브라우저를 종료해도 계속 남는다. 하지만 세션은 만료시간을 정할 수 있지만 &lt;b&gt;브라우저가 종료되면 만료시간에 상관없이 삭제된다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;매 요청마다 세션스토리지를 들어다보고 비교해야 하므로 비용이 비싸다.&lt;/li&gt;
&lt;li&gt;추후 로드밸런싱을 하면 서버마다 세션ID가 달라서 난감하다 따라서 세션을 저장하는 하나의 스토리지에 모아야 하는데 이러면 모든 클라이언트의 요청마다 해당 스토리지로 오기 때문에 터질 가능성이 크다. 즉, 확장성이 어렵다.&lt;/li&gt;
&lt;li&gt;동접자가 많은 서비스라면 요청의 부하가 커지므로 위험할 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;토큰&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7gIQK/btrJRtkgWjC/DHLpVaTO364kGoH6lLrr30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7gIQK/btrJRtkgWjC/DHLpVaTO364kGoH6lLrr30/img.png&quot; data-alt=&quot;토큰 flow&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7gIQK/btrJRtkgWjC/DHLpVaTO364kGoH6lLrr30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7gIQK%2FbtrJRtkgWjC%2FDHLpVaTO364kGoH6lLrr30%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;732&quot; height=&quot;354&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;토큰 flow&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 &lt;b&gt;인증을 위한 정보를 서명&lt;/b&gt;한 것이다. 세션 기반의 인증이 클라이언트의 상태를 서버에 저장하고 요청마다 세션 스토리지에 저장된 유효한 세션인지 확인한다. 반면에 토큰 기반 인증은 토큰에 사용자 인증을 위한 정보가 담겨있으므로 서버에 사용자 정보를 저장하지 않고, 전달받은 토큰의 서명과 데이터를 검증하는 것만으로 인증이 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰 메커니즘&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 로그인을 한다.&lt;/li&gt;
&lt;li&gt;서버 측에서 해당 정보를 검증한다.&lt;/li&gt;
&lt;li&gt;정보가 정확하다면 서버 측에서 비밀키 또는 공개/개인 키를 이용해 서명한 토큰을 사용자에게 발급한다.&lt;/li&gt;
&lt;li&gt;클라이언트는 전달받은 토큰을 저장해 두고, 서버에 요청할 때마다 해당 토큰을 전달한다.&lt;/li&gt;
&lt;li&gt;서버는 토큰의 서명 값을 이용하여 검증하고 요청에 응답한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stateless(무상태성)과 확장성이 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;토큰이 클라이언트에 저장되므로 서버는 Stateless하며, 클라이언트와 세션간의 stateful하지 않으므로 확장하기 쉽다.&lt;/li&gt;
&lt;li&gt;세션과 달리 각 서버가 해당 시크릿키로 인증을 하면 되므로 세션 스토리지같은 서버를 가지고 있지 않아도 된다.&lt;/li&gt;
&lt;li&gt;저장 없이 유효한 토큰인지만 검증하므로 다른 플랫폼, 서비스 간에도 사용하기 편하다. ex) OAuth&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;토큰이 털린다면 해커가 사용자인척 요청을 할 수 있다. 따라서 만료기간을 짧게 해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT 토큰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #2b2b2b;&quot;&gt;JWT는 JSON Web Token의 약자로 전자 서명된 URL-safe (URL로 이용할 수 있는 문자만 구성된)의 JSON입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #2b2b2b;&quot;&gt;전자 서명은 JSON의 변조를 체크할 수 있게 되어 있습니다. base64url로 인코딩하는 이유도 jwt가 어느 곳으로 보내든, url, header, cookie에 보내든 사용이 가능하도록 하기 위함입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #2b2b2b;&quot;&gt;jwt는 header, payload, signiture로 이루어져 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #2b2b2b;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;header에는 토큰 타입과 해시 암호화 알고리즘이 들어있습니다. payload에는 토큰에 담을 claim(클레임)을 가지고 있습니다. claim은 &lt;span style=&quot;background-color: #ffffff; color: #2b2b2b;&quot;&gt;name / value 의 한 쌍으로 이뤄져 있습니다. 토큰에는 여러 개의 클레임들을 넣을 수 있습니다. 클레임은&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #2b2b2b;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #2b2b2b;&quot;&gt; 등&lt;span style=&quot;background-color: #ffffff; color: #2b2b2b;&quot;&gt;록된(registered) 클레임, 공개(public) 클레임, 비공개(private) 클레임으로 종류가 나뉩니다. 일반적으로 등록된 클레임은 입력 값으로 무엇을 받아야 하는지가 틀이 잡혀있어서 주로 사용합니다.&lt;/span&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #2b2b2b;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;토큰 전략&lt;/span&gt;&lt;/span&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;slidingSession전략&lt;/b&gt;은 페이지를 이동할 때마다 AccessToken을 재발급해주는 방식이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;refreshToken&lt;/b&gt;을 두어 AccessToken이 만료되면 리프레시 토큰을 확인하여 재발급한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;refreshTokenRoation(RTR)&lt;/b&gt;방식으로 accessToken이 만료될 때마다 refreshToken도 재발급한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rtr은 refreshToken으로부터 한 번만 accessToken을 발급하도록 하는 방식이다. 만약 refreshToken으로 한 번 더 accessToken이 발급되면 해킹당했다고 판단할 수 있다. 따라서 rtr을 사용하려면 디비저장이 불가피하다고 생각된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방식 중 서비스에 적합하다고 생각되는 방식을 사용하면 된다. 다만 refreshToken을 디비에 저장하지 않는다면 탈취당했을 때, 발급된 refreshToken을 유효하지 않게 만들 방법이 없으므로 결국엔 디비에 저장을 해야 하는가 싶다. 그런데 또 디비에 저장하면 토큰의 장점이 사라지게 되고 그렇다면 세션을 사용하는 게 토큰보다 정보 노출이 적고, 데이터 길이도 짧아서 통신에 더 좋지 않나?라는 생각이 든다.&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;&lt;a title=&quot;다음 글&quot; href=&quot;https://giron.tistory.com/136&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://giron.tistory.com/136&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://gcs.emro.co.kr:8090/pages/viewpage.action?pageId=9873130&quot;&gt;http://gcs.emro.co.kr:8090/pages/viewpage.action?pageId=9873130&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC#Cookie_%EC%9D%B8%EC%A6%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC#Cookie_%EC%9D%B8%EC%A6%9D&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-import-annotation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/spring-import-annotation&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>HTTP</category>
      <category>세션</category>
      <category>쿠키</category>
      <category>토큰</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/135</guid>
      <comments>https://giron.tistory.com/135#entry135comment</comments>
      <pubDate>Tue, 23 Aug 2022 21:09:10 +0900</pubDate>
    </item>
    <item>
      <title>소나큐브 적용기</title>
      <link>https://giron.tistory.com/134</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;SonarQube 란&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적 코드 분석을 자동을 수행하는 오픈 소스 플랫폼&lt;/li&gt;
&lt;li&gt;중복 코드, 코딩 표준, 유닛 테스트, 코드 커버리지, 코드 복잡도, 주석, 버그 및 보안 취약점에 대해서 검사하고 결과를 보고서로 작성한다.&lt;/li&gt;
&lt;li&gt;20개 이상의 언어에 대한 분석이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정적 코드 분석 이란&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로그램을 실행하지 않고 소스 코드나 컴파일된 코드를 분석하는 작업&lt;/li&gt;
&lt;li&gt;프로그램을 실행하지 않고 버그나 취약점을 분석할 수 있다.&lt;/li&gt;
&lt;li&gt;주로 개발 단계에서 코드의 구조적인 문제나 실수를 찾아내기 위해서 사용한다.&lt;/li&gt;
&lt;li&gt;코드 작성단계에서 차후 코드를 실행했을 때 발생할 가능성이 높은 문제를 미리 찾고 대처할 수 있다.&lt;/li&gt;
&lt;li&gt;단순이 버그나 오류를 찾아내는 것 뿐만 아니라 더 좋은 코드를 위한 개선점을 제시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 동적 분석에는 모니터링이나 테스트가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정적 코드 분석을 위해 SonarQube 를 사용한 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SonarCloud도 있었지만 SonarColud같은 경우는 소나큐브에 비해 한정적 기능을 제공한다.&lt;/li&gt;
&lt;li&gt;레퍼런스가 많다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sonar cloud가 나오기 훨씬 전부터 나왔으므로 안정적이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;오픈 소스 플랫폼이기 때문에 기본적으로 무료로 제공되고 레퍼런스가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;20개 이상의 다양한 언어에 대한 검사가 가능하기 때문에 다른 언어로 작성할 때도 SonarQube 를 정적 분석 도구로 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;GitHub, Jenkins 등의 툴과 다양한 방식으로 연동이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SonarQube 동작 개념&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SonarQube Scanner 가 소스코드를 Scan 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gradle, Jenkins 등 다양한 플랫폼에서 SonarQube Scanner 를 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SonarQube 어플리케이션이 설치된 서버로 Scan 한 소스코드를 전달한다.&lt;/li&gt;
&lt;li&gt;SonarQube 어플리케이션이 전달받은 소스코드를 분석하여 보고서를 작성한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SonarQube 사용 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 환경&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub &amp;amp; GitHub Actions&lt;/li&gt;
&lt;li&gt;Gradle&lt;/li&gt;
&lt;li&gt;Aws ec2
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ubuntu 22.04&lt;/li&gt;
&lt;li&gt;Arm 아키텍쳐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;java 11&lt;/li&gt;
&lt;li&gt;SonarQube 8.9.9 (lts버전)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 개요&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 GitHub Repository 에 PR 을 열거나 이미 열려있는 PR 의 branch 에 새로운 commit 을 push 한다.&lt;/li&gt;
&lt;li&gt;GitHub Actions 를 통해 자동으로 Gradle 의 Scanner Task 가 실행된다.&lt;/li&gt;
&lt;li&gt;Scan 된 소스코드가 SonarQube 어플리케이션이 설치된 ec2 서버로 전달된다.&lt;/li&gt;
&lt;li&gt;SonarQube 어플리케이션이 전달받은 소스코드를 분석하여 보고서를 작성한다.&lt;/li&gt;
&lt;li&gt;보고서 작성이 끝나면 GitHub PR 에 보고서 링크를 남긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;ec2 인스턴스에 SonarQube 설치 &amp;amp; 실행&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1660265586969&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// apt 업데이트 &amp;amp; unzip 설치

$ sudo apt update
$ sudo apt install unzip&lt;/code&gt;&lt;/pre&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;소나큐브는 자바로 만들어져있어서 자바8 이상을 설치해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1660265614062&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 자바 11 설치

$ wget -O- https://apt.corretto.aws/corretto.key | sudo apt-key add - 
 sudo add-apt-repository 'deb https://apt.corretto.aws stable main'
$ sudo apt-get update; sudo apt-get install -y java-11-amazon-corretto-jdk&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;pre id=&quot;code_1660265692472&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SonarQube 를 설치할 폴더 생성, 이동

$ sudo mkdir app
$ cd app
$ sudo mkdir sonarqube
$ cd sonarqube&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1660265732726&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// wget 으로 소나큐브 설치, 압축 해제(설치)

$ sudo wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-8.9.9.56886.zip
$ sudo unzip sonarqube-8.9.9.56886.zip&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SonarQube 를 sudo 로 설치했기 때문에 SonarQube 의 실행을 위해서는 root 권한이 있어야한다.&lt;/li&gt;
&lt;li&gt;나의 root user 이름은 ubuntu 였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1660265848531&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SonarQube 실행 권한 변경

$ sudo chown -R ubuntu /app/sonarqube&lt;/code&gt;&lt;/pre&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;8081로 포트 번호 수정 (우테코에서는 8080과 8081만 인바운드를 열어줘서 8081로 바꿨다)기본 port 번호는 9000이다.&lt;/p&gt;
&lt;pre id=&quot;code_1660265898292&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// sonar.properties 파일 에서 소나큐브 포트 수정

vi /app/sonarqube/sonarqube-8.9.9.56886/conf/sonar.properties&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1660265928020&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
sonar.web.port=8081
...&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;pre id=&quot;code_1660266010308&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// lib 폴더에서 직접 소나큐브의 jar 파일 실행

cd /app/sonarqube/sonarqube-8.9.9.56886/lib
nohup java -jar sonar-application-8.9.9.56886.jar &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 /app/sonarqube/sonarqube-8.9.9.56886/bin 디렉토리에서 sonar.sh파일을 실행시키면 실행이 된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저희 인스턴스가 arm아키텍처를 사용하므로 jar파일을 직접 실행시켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소나큐브가 아직 arm아키텍처를 지원하지 않는다고 한다.&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;이제 소나큐브에 ip:8081을 통해서 접근이 가능하다. &lt;b&gt;기본 계정의 아이디와 비밀번호는 admin&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;SonarQube 어플리케이션에 접근하여 우측 상단의 A를 클릭 -&amp;gt; My Account에 들어간다.&lt;/li&gt;
&lt;li&gt;Security 탭에서 토큰을 생성한다. 생성된 토큰을 저장해둔다.&lt;/li&gt;
&lt;li&gt;생성된 Token 문자열을 잘 저장해두고 차후 Scanner 를 발동시킬 때 사용한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그후, projects에 들어가서 프로젝트도 만들어주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D2aMz/btrJuRGb8EK/3ZzYXNEKU0BdUsA3RWK6F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D2aMz/btrJuRGb8EK/3ZzYXNEKU0BdUsA3RWK6F0/img.png&quot; data-alt=&quot;토큰 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D2aMz/btrJuRGb8EK/3ZzYXNEKU0BdUsA3RWK6F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD2aMz%2FbtrJuRGb8EK%2F3ZzYXNEKU0BdUsA3RWK6F0%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;768&quot; height=&quot;351&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;토큰 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle 파일에 Scanner 설정 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1660266515046&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'org.sonarqube' version '3.3' 
    id 'java'
    id 'jacoco'
}

sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3'
}

test {
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}

jacoco { 
    toolVersion = &quot;0.8.7&quot;
}

jacocoTestReport { 
    reports {
        xml.enabled true
        csv.enabled false
        html.enabled false
    }
}

sonarqube { 
    properties {
        property &quot;sonar.host.url&quot;, &quot;SonarQube 서버 url&quot;
        property &quot;sonar.login&quot;, &quot;SonarQube 어플리케이션에서 생성한 token&quot;
        property &quot;sonar.projectKey&quot;, &quot;프로젝트 이름&quot;
        property &quot;sonar.projectName&quot; , &quot;SonarQube 로 전달할 소스 코드의 이름 or 버전&quot;
        property &quot;sonar.sources&quot;, &quot;src&quot; // 소스 경로
        property &quot;sonar.language&quot;, &quot;java&quot; // 언어
        property &quot;sonar.sourceEncoding&quot;, &quot;UTF-8&quot;
        property &quot;sonar.profile&quot;, &quot;Sonar way&quot; // SonarQube 에서 분석할 때 적용할 프로필(분석할 수준 설정)
        property &quot;sonar.java.binaries&quot;, &quot;${buildDir}/classes&quot; // 자바 클래스 파일위치
        property &quot;sonar.test.inclusions&quot;, &quot;**/*Test.java&quot; // 코드 분석에 사용할 테스트 소스 
        property &quot;sonar.coverage.jacoco.xmlReportPaths&quot;, &quot;${buildDir}/reports/jacoco/test/jacocoTestReport.xml&quot; // jacoco 플러그인의 결과 파일
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;sonarqube 서버 url이나 login같은 경우는 노출되면 안되므로 해당 property를 지우고 환경변수로 넣어서 사용하면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;명령어같은 경우는 소나큐브에 들어가면 친절히 알려준다.&lt;/p&gt;
&lt;pre id=&quot;code_1660266648777&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew -Dsonar.login={token} -Dsonar.host.url={sonarqube url}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Scanner 발동을 위한 GitHub Actions 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 GitHub Repository 에 PR 을 열거나 이미 열려있는 PR 의 branch 에 새로운 commit 을 push 할 때 Gradle 의 Scanner 를 발동시켜 정적 분석을 자동으로 수행하기 위해서는 GitHub Actions 의 Workflow 를 사용해야 한다.&lt;/li&gt;
&lt;li&gt;GitHub Actions 의 Workflow yml 파일 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.github/workflow/sonarqube.yml 파일&lt;/h4&gt;
&lt;pre id=&quot;code_1660213335197&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: gongseek-backend-sonarqube

# 하기 내용에 해당하는 이벤트 발생 시 github action 동작
on:
  pull_request: # 모든 브랜치에서 PR이 일어났을 때 github action 동작
    branches:
      - '*'
    types: [opened, synchronize]
defaults:
  run:
    working-directory: ./backend

jobs:
  build:
    runs-on: ubuntu-22.04 # 실행 환경 지정

    steps:
      - name: Checkout source code
        uses: actions/checkout@v2
  analysis:
    runs-on: ubuntu-22.04
    env:
      SONARQUBE_PROJECT_KEY: gongseek
      SONARQUBE_URL: ${{secrets.SONARQUBE_URL}}
      SONARQUBE_TOKEN: ${{secrets.SONARQUBE_TOKEN}}
      PR_NUMBER: ${{github.event.pull_request.number}}
    steps:
      # 소스코드 체크아웃 수행
      - name: Checkout source code
        uses: actions/checkout@v2

      # gradlw 파일 권한 변경
      - name: gradlew permission change
        run: sudo chmod 755 gradlew

      # Gralde 의 Scanner 발동, 위의 env 에서 선언한 환경변수와 함께 발동
      - name: Sonaqube Analysis
        run: ./gradlew test sonarqube
          -Dsonar.host.url=${{ env.SONARQUBE_URL }}
          -Dsonar.projectKey=${{ env.SONARQUBE_PROJECT_KEY }}
          -Dsonar.projectName=${{ env.SONARQUBE_PROJECT_KEY }}-${{ env.PR_NUMBER }}
          -Dsonar.login=${{ env.SONARQUBE_TOKEN }}
      # PR 에 Comment 를 달아주는 스크립트 실행
      - name: Comment Sonarqube URL
        uses: actions/github-script@v4
        with:
          script: |
            const { SONARQUBE_PROJECT_KEY, SONARQUBE_URL, PR_NUMBER } = process.env
            github.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `  ${ SONARQUBE_PROJECT_KEY }-${ PR_NUMBER } 분석 결과 확인하기 [링크](${SONARQUBE_URL})`
            })&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이제 거의 다 했다. 환경변수를 넣어주기만 하면 된다.&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Github Actions에 환경변수 설정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/muIPO/btrJA4salEY/xp7KxKyUggfxCIdKdxyiy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/muIPO/btrJA4salEY/xp7KxKyUggfxCIdKdxyiy0/img.png&quot; data-alt=&quot;레포지토리의 settings로 이동&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/muIPO/btrJA4salEY/xp7KxKyUggfxCIdKdxyiy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmuIPO%2FbtrJA4salEY%2Fxp7KxKyUggfxCIdKdxyiy0%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;1270&quot; height=&quot;789&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레포지토리의 settings로 이동&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new repository secret을 클릭해서 토큰을 입력해주면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;소나큐브 실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nohup&amp;nbsp;java&amp;nbsp;-jar&amp;nbsp;sonar-application-8.9.9.56886.jar&amp;nbsp;&amp;amp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://astonishing-message-fd2.notion.site/SonarQube-87663a36f4604441a56dca3357251f9b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://astonishing-message-fd2.notion.site/SonarQube-87663a36f4604441a56dca3357251f9b&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2661/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://techblog.woowahan.com/2661/&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>jacoco</category>
      <category>SonarQube</category>
      <category>소나큐브</category>
      <category>정적파일분석</category>
      <category>테스트커버리지</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/134</guid>
      <comments>https://giron.tistory.com/134#entry134comment</comments>
      <pubDate>Mon, 15 Aug 2022 23:38:56 +0900</pubDate>
    </item>
    <item>
      <title>[테스트 자동화 1] service 테스트에서 롤백 목적으로 @Transactional 사용을 지양하자! 그런데 EntityManager를 활용해서 truncate를 시켜보자</title>
      <link>https://giron.tistory.com/133</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로덕션&amp;nbsp;코드에서&amp;nbsp;게시글&amp;nbsp;조회&amp;nbsp;로직이&amp;nbsp;있었습니다.&amp;nbsp;게시글을&amp;nbsp;조회할&amp;nbsp;때,&amp;nbsp;조회수&amp;nbsp;증가(update)가&amp;nbsp;있었지만&amp;nbsp;단순히&amp;nbsp;조회하는&amp;nbsp;로직이라고&amp;nbsp;생각했고&amp;nbsp;@Transactional(readOnly=true)옵션을&amp;nbsp;설정해주었습니다.&amp;nbsp;그리고&amp;nbsp;서비스&amp;nbsp;통합&amp;nbsp;테스트에서는&amp;nbsp;@Transactional을&amp;nbsp;통한&amp;nbsp;롤백을&amp;nbsp;사용했었습니다.&amp;nbsp;&amp;nbsp;그래서&amp;nbsp;테스트에서는&amp;nbsp;정상적으로&amp;nbsp;조회수가&amp;nbsp;증가해서&amp;nbsp;이상이&amp;nbsp;없었지만&amp;nbsp;실제&amp;nbsp;QA할&amp;nbsp;때,&amp;nbsp;조회수가&amp;nbsp;증가하지&amp;nbsp;않는&amp;nbsp;버그를&amp;nbsp;발견했습니다.&amp;nbsp;이러한&amp;nbsp;문제를&amp;nbsp;어떻게&amp;nbsp;해결할까&amp;nbsp;고민을&amp;nbsp;했었고,&amp;nbsp;서비스&amp;nbsp;통합테스트에서&amp;nbsp;@Transactional을&amp;nbsp;제거해서&amp;nbsp;이와&amp;nbsp;같은&amp;nbsp;실수가&amp;nbsp;재발하는&amp;nbsp;것을&amp;nbsp;방지했습니다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NJmR0/btrIsQOJZSK/rQq5J498VzyNfWBrBDlAk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NJmR0/btrIsQOJZSK/rQq5J498VzyNfWBrBDlAk1/img.png&quot; data-alt=&quot;readOnly옵션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NJmR0/btrIsQOJZSK/rQq5J498VzyNfWBrBDlAk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNJmR0%2FbtrIsQOJZSK%2FrQq5J498VzyNfWBrBDlAk1%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;882&quot; height=&quot;276&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;readOnly옵션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드를 보고 단순히 조회니깐 readOnly=true를 붙이면 되겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Service 테스트에서 @SpringBootTest와 @Transactional을 걸었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBnAe7/btrIturaUhL/pKzZALKGOYgMuy9IfaqF0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBnAe7/btrIturaUhL/pKzZALKGOYgMuy9IfaqF0k/img.png&quot; data-alt=&quot;테스트 내용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBnAe7/btrIturaUhL/pKzZALKGOYgMuy9IfaqF0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBnAe7%2FbtrIturaUhL%2FpKzZALKGOYgMuy9IfaqF0k%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;1069&quot; height=&quot;395&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;395&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 내용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 테스트에서 조회수가 2번 증가하는 것을 확인했다. 하지만 프로덕션에서 돌렸을 때는 조회수가 증가하지 않았다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 처음 위에 메서드는 내부에 &lt;b&gt;조회수를 올리는 로직이 있는데&amp;nbsp;&lt;/b&gt;readOnly가 걸려있어서 더티체킹이 제대로 되지 않는 이슈였기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서비스 테스트에서 @Transactional을 걸면 Transactional에 대한 제대로 된 테스트가 어렵다는 것을 확인했다.&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px;&quot;&gt;정리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로덕션 코드에서 조회수 증가 로직에 Transactional을 잘못걸었지만 테스트시엔 serviceTest위에 @Transactional이 걸려있어서 테스트가 통과되었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 실제 QA를 진행해보니 버그가 있다는 것을 발견했고, 테스트코드의 정확성을 높이기 위해 @Transactional을 제거하려고 한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 어느 범위까지 하느냐가 @Transactional 설정 여부를 결정할수 있을 것 같다. 만약&lt;span style=&quot;color: #000000;&quot;&gt; Service layer의 테스트에서 '@Transactional&amp;nbsp;어노테이션의 동작까지 함께 테스트한다'를 목표로 한다면 @Transactional을 제거하고 하는 게 맞고,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Transactional을 잘 걸엇다고 자신이 있다면 @Trasanctional로 쉽게 테스트를 해도 될 것이다.&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: #000000;&quot;&gt;또한 @DataJpaTest할 때는 Transactional이 default로 걸려있는데 해당 부분은 단&lt;span style=&quot;color: #000000;&quot;&gt;순히&amp;nbsp;JpaRepository의 기능을 테스트하기 위함이므로&amp;nbsp;@DataJpaTest를 이용하여&amp;nbsp;@Transactional의 rollback 기능을 사용해도 무관하다고 생각한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;그중 첫 번째는 @AfterEach 혹은 @BeforeEach로 모든 Repository에 대해서 deleteAll()을 실행해주는 것이다. 해당 방법은 Repository가 늘어날 때마다 추가해야 하는 단점이 있다.&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;두 번째로는 인수 테스트 격리에서 사용한 방법처럼 EntityManager를 이용한 Truncate 하는 방법이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1659068670507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class DatabaseCleaner {

    @PersistenceContext
    private EntityManager entityManager;

    private List&amp;lt;String&amp;gt; tableNames;

    @PostConstruct
    public void afterPropertiesSet() {
        tableNames = entityManager.getMetamodel().getEntities().stream()
                .filter(entityType -&amp;gt; entityType.getJavaType().getAnnotation(Entity.class) != null)
                .map(entityType -&amp;gt; changeNaming(entityType.getName()))
                .collect(Collectors.toList());
    }

    private String changeNaming(String entityName) {
        StringBuilder tableName = new StringBuilder();
        for (int index = 0; index &amp;lt; entityName.length(); index++) {
            char ch = entityName.charAt(index);
            if (index &amp;gt; 0 &amp;amp;&amp;amp; Character.isUpperCase(ch)) {
                tableName.append(&quot;_&quot;);
            }
            tableName.append(Character.toLowerCase(ch));
        }
        return tableName.toString();
    }

    @Transactional
    public void tableClear() {
        entityManager.createNativeQuery(&quot;SET FOREIGN_KEY_CHECKS = 0&quot;).executeUpdate();

        for (String tableName : tableNames) {
            entityManager.createNativeQuery(&quot;TRUNCATE TABLE &quot; + tableName).executeUpdate();
        }
        entityManager.createNativeQuery(&quot;SET FOREIGN_KEY_CHECKS = 1&quot;).executeUpdate();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 엔티티를 확인하면서 table이름으로 truncate를 시켜준다.&amp;nbsp;위와 같이 설정해준다면 Repository가 늘어날 때마다 코드를 수정해줄 일이 없어서 편해진다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA만 국한된 기술이 아닌 JDBC를 이용할때도 아래처럼 metaData를 통해 truncate를 할수있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class DatabaseCleaner {

    private static final String TRUNCATE_QUERY = &quot;TRUNCATE TABLE %s&quot;;

    @Autowired
    private DataSource dataSource;

    private final List&amp;lt;String&amp;gt; tableNames = new ArrayList&amp;lt;&amp;gt;();

    @PostConstruct
    public void afterPropertiesSet() {
        try (Connection connection = dataSource.getConnection();) {
            DatabaseMetaData metaData = connection.getMetaData();
            final ResultSet tables = metaData.getTables(null, null, null, new String[]{&quot;TABLE&quot;});
            while (tables.next()) {
                tableNames.add(tables.getString(&quot;TABLE_NAME&quot;));
            }
        } catch (SQLException e) {
            throw new NoSuchElementException();
        }
        tableNames.remove(&quot;flyway_schema_history&quot;);
    }

    @Transactional
    public void clear() {
        try (final Connection connection = dataSource.getConnection()) {
            connection.prepareStatement(&quot;SET REFERENTIAL_INTEGRITY FALSE&quot;).execute();
            for (String tableName : tableNames) {
                connection.prepareStatement(String.format(TRUNCATE_QUERY, tableName)).execute();
            }
            connection.prepareStatement(&quot;SET REFERENTIAL_INTEGRITY TRUE&quot;).execute();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}&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;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://junit.org/junit5/docs/5.0.3/api/org/junit/jupiter/api/extension/BeforeEachCallback.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://junit.org/junit5/docs/5.0.3/api/org/junit/jupiter/api/extension/BeforeEachCallback.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2020-08-31-jpa-transaction-test/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tecoble.techcourse.co.kr/post/2020-08-31-jpa-transaction-test/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1659069164785&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;JPA 사용시 테스트 코드에서 @Transactional 주의하기&quot; data-og-description=&quot;서비스 레이어()에 대해 테스트를 한다면 보통 DB&amp;hellip;&quot; data-og-host=&quot;tecoble.techcourse.co.kr&quot; data-og-source-url=&quot;https://tecoble.techcourse.co.kr/post/2020-08-31-jpa-transaction-test/&quot; data-og-url=&quot;https://post/2020-08-31-jpa-transaction-test/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cWQ4V4/hyPeNYiFf2/BdVIyETdALkv5xrRrPZZE1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/bGNlef/hyPgadgYbf/ZlAfKizP57cYF1cx2LWtMK/img.png?width=1448&amp;amp;height=1840&amp;amp;face=0_0_1448_1840&quot;&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2020-08-31-jpa-transaction-test/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tecoble.techcourse.co.kr/post/2020-08-31-jpa-transaction-test/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cWQ4V4/hyPeNYiFf2/BdVIyETdALkv5xrRrPZZE1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/bGNlef/hyPgadgYbf/ZlAfKizP57cYF1cx2LWtMK/img.png?width=1448&amp;amp;height=1840&amp;amp;face=0_0_1448_1840');&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;JPA 사용시 테스트 코드에서 @Transactional 주의하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서비스 레이어()에 대해 테스트를 한다면 보통 DB&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tecoble.techcourse.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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>@Transactional</category>
      <category>service Test</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/133</guid>
      <comments>https://giron.tistory.com/133#entry133comment</comments>
      <pubDate>Fri, 29 Jul 2022 13:35:59 +0900</pubDate>
    </item>
    <item>
      <title>HTTPS 적용하기</title>
      <link>https://giron.tistory.com/132</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 진행하면서 HTTPS 적용기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터넷 상에서 정보를 주고 받기위한 프로토콜(양식과 규칙의 체계)&lt;/li&gt;
&lt;li&gt;클라이언트와 서버 사이에 이루어지는 요청/응답 프로토콜&lt;/li&gt;
&lt;li&gt;암호화되지 않은 평문으로 데이터를 전송한다. (악의적인 감청, 데이터 변조의 가능성)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTTPS
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP + secure&lt;/li&gt;
&lt;li&gt;모든 HTTP 요청과 응답 데이터는 네트워크로 보내지기 전에 암호화된다.&lt;/li&gt;
&lt;li&gt;HTTPS는 HTTP의 하부에 SSL과 같은 보안계층을 제공함으로써 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도메인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비아에서 도메인을 구입하면 TTL을 설정할 수 있다. TTL에 대해서 학교에서 배웠는데 기억이 안나서 다시 찾아보았다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b31jJx/btrIzEgkarp/IL0e2f19CRfVNKn3HECUzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b31jJx/btrIzEgkarp/IL0e2f19CRfVNKn3HECUzK/img.png&quot; data-alt=&quot;ttl&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b31jJx/btrIzEgkarp/IL0e2f19CRfVNKn3HECUzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb31jJx%2FbtrIzEgkarp%2FIL0e2f19CRfVNKn3HECUzK%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;975&quot; height=&quot;559&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ttl&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TimeToLive의 약자로 Routing&amp;nbsp; Loop를 방지하여 무한 루프에 빠지는 것을 막을 수 있다. 또한 멀티 캐스팅할 때, 라우터 범위를 정해서 멀티 캐스트 범위를 제한할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;대칭키 와 비대칭 키 요약&lt;/h4&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;543&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyWQaY/btrIBUwbote/4e0xRw8kYjjY71PtX35fX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyWQaY/btrIBUwbote/4e0xRw8kYjjY71PtX35fX1/img.png&quot; data-alt=&quot;정리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyWQaY/btrIBUwbote/4e0xRw8kYjjY71PtX35fX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyWQaY%2FbtrIBUwbote%2F4e0xRw8kYjjY71PtX35fX1%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;543&quot; height=&quot;463&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CA를 통한 SSL 인증 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버, CA, 클라이언트가 있을 때,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버는 &lt;b&gt;자신의 정보와 공개키&lt;/b&gt;를 CA에 보냅니다.&lt;/li&gt;
&lt;li&gt;CA는 해당 정보를 확인 후, 정상적인 사이트로 판단되면 CA의 &lt;b&gt;개인키로 암호화한 인증서를 서버에 보냅니다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;그리고 &lt;b&gt;CA는&lt;/b&gt; 자신의 &lt;b&gt;공개키&lt;/b&gt;를 클라이언트의 브라우저에 줍니다. -&amp;gt; 브라우저는 해당 키를 저장해둡니다.&lt;/li&gt;
&lt;li&gt;유저가 브라우저에 접속하여 사이트에 접근을 합니다.&lt;/li&gt;
&lt;li&gt;이때 사이트는 인증서를 클라이언트에게 전달해줍니다.&lt;/li&gt;
&lt;li&gt;클라이언트는&lt;b&gt; CA의 공개키&lt;/b&gt;를 통해 해당 &lt;b&gt;인증서를 복호화&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사이트의 정보를 확인&lt;/b&gt;하고 &lt;b&gt;자신의 대칭키&lt;/b&gt;를 인증서 안에 있던 &lt;b&gt;서버의 공개키로 암호화&lt;/b&gt;해서 서버에 전달합니다.&lt;/li&gt;
&lt;li&gt;서버는 자신의 &lt;b&gt;개인키&lt;/b&gt;로 전달받은 암호문을 복호화합니다. 그러면 &lt;b&gt;사용자의 대칭키를 얻게&lt;/b&gt; 됩니다.&lt;/li&gt;
&lt;li&gt;이제 대칭키를 통해서 통신을 합니다!&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;certbot을 통한 인증서 발급&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ sudo apt-get update&lt;br /&gt;$ sudo apt-get install software-properties-common&lt;br /&gt;$ sudo add-apt-repository universe&lt;br /&gt;$ sudo apt-get update&lt;br /&gt;$ sudo apt-get install certbot python3-certbot-nginx&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프론트엔드 서버 (EC2 - ubuntu)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ cd /etc/nginx/site-avaliable&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 위치에서 myapp.conf라는 설정 파일을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ sudo vi myapp.conf&lt;/p&gt;
&lt;pre id=&quot;code_1658984993074&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
  server_name gongseek.site;

  location / {
    root   /home/ubuntu/dist; //프론트 정적파일 경로
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;$ sudo certbot --test-cert --nginx -d gongseek.site ( 인증서 발급 테스트를 진행한다.)&lt;br /&gt;$ sudo certbot certificates&amp;nbsp;-&amp;gt; 인증서 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ sudo certbot --nginx -d gongseek.site -&amp;gt; 만약 위 경우에 인증서를 여러번 발급했다고 아래 사진처럼 실패한다면 아래 명령어를 계속 진행한다( 그전에 가비아에서 서브도메인으로 기존에 그냥 gongseek.site였다면 www.을 붙여서 하나 만들어준다.)&lt;br /&gt;$ cd /etc/nginx/site-avaliable/&lt;br /&gt;$ sudo vi myapp.conf&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;-&amp;gt; server_name &lt;a href=&quot;http://gongseek.site&quot;&gt;gongseek.site&lt;/a&gt; &lt;a href=&quot;http://www.gongseek.site;&quot;&gt;www.gongseek.site;&lt;/a&gt;추가한다.&lt;br /&gt;$ sudo certbot --nginx -d &lt;a href=&quot;http://gongseek.site&quot;&gt;gongseek.site&lt;/a&gt; -d&amp;nbsp;&lt;a href=&quot;http://www.gongseek.site&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;www.gongseek.site&lt;/a&gt; &lt;br /&gt;$ sudo certbot certificates -&amp;gt; 인증서 확인&lt;br /&gt;$ sudo nginx -s reload&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;이후 웹사이트에 접속하면 https가 적용된 것을 확인할 수 있다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;71&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KU5d5/btrIlw4qou2/h9gJCxlzmZpNrRmw92E20K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KU5d5/btrIlw4qou2/h9gJCxlzmZpNrRmw92E20K/img.png&quot; data-alt=&quot;인증서 발급 제한&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KU5d5/btrIlw4qou2/h9gJCxlzmZpNrRmw92E20K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKU5d5%2FbtrIlw4qou2%2Fh9gJCxlzmZpNrRmw92E20K%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;841&quot; height=&quot;71&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;71&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인증서 발급 제한&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 가비아에 들어가서 서브 도메인을 만들면 된다. 저희 팀은 &lt;a href=&quot;http://gongseek.site에서&quot;&gt;gongseek.site에서&lt;/a&gt; &lt;a href=&quot;http://www.gongseek.site&quot;&gt;www.gongseek.site&lt;/a&gt;로 만들어서 해결했습니다. 그 후, server_name에 &lt;a href=&quot;http://gongseek.site&quot;&gt;gongseek.site&lt;/a&gt; &lt;a href=&quot;http://www.gongseek.site;&quot;&gt;www.gongseek.site;&amp;nbsp;&lt;/a&gt; 로 해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 백엔드 서버 (EC2 - ubuntu) 내부에 jar파일 있을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드의 도메인으로 프론트의 도메인과 다른 백엔드의 서브 도메인으로 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 내부에 jar파일이 있으므로 127.0.0.1로 연결해준다. 만약 nginx를 다른 서버에 둔다면 jar파일이 있는 서버의 주소로 proxy_pass를 설정하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비아에서 back.gongseek.site를 백앤드 ec2에 연결해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1658996553334&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
        server_name back.gongseek.site;
        location /{
                proxy_pass http://127.0.0.1:8080;
        }
}&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;$ sudo ln /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ sudo nginx -s reload&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ sudo nginx -t (nginx 상태 확인)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ sudo certbot --test-cert --nginx -d back.gongseek.site (인증서 테스트를 해본다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; front의 도메인과 다른 백엔드의 서브 도메인을 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ sudo certbot --nginx -d back.gongseek.site ( 실제 인증서를 받는 작업이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도한다면 2번으로 하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;인증서 자동갱신&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서를 매월 1일마다 갱신되도록 해봅시다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;color: #212529;&quot;&gt;$ sudo nano /etc/crontab&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;# /etc/crontab: system-wide crontab&lt;br /&gt;# Unlike any other crontab you don't have to run the `crontab'&lt;br /&gt;# command to install the new version when you edit this file&lt;br /&gt;# and files in /etc/cron.d. These files also have username fields,&lt;br /&gt;# that none of the other crontabs do.&lt;br /&gt;&lt;br /&gt;SHELL=/bin/sh&lt;br /&gt;PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin&lt;br /&gt;&lt;br /&gt;# m h dom mon dow user command&lt;br /&gt;17 *&amp;nbsp; &amp;nbsp;* * * root cd / &amp;amp;&amp;amp; run-parts --report /etc/cron.hourly&lt;br /&gt;25 6 *&amp;nbsp; &amp;nbsp;* * root test -x /usr/sbin/anacron || ( cd / &amp;amp;&amp;amp; run-parts --report /etc/cron.daily )&lt;br /&gt;47 6 *&amp;nbsp; &amp;nbsp;* 7 root test -x /usr/sbin/anacron || ( cd / &amp;amp;&amp;amp; run-parts --report /etc/cron.weekly )&lt;br /&gt;52 6 1 * * root test -x /usr/sbin/anacron || ( cd / &amp;amp;&amp;amp; run-parts --report /etc/cron.monthly )&lt;br /&gt;0 0 1 * * root certbot renew --quiet &amp;lt;- 해당 부분을 추가해준다.&lt;br /&gt;#&lt;br /&gt;자동갱신은 설정&lt;/blockquote&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;자동갱신을 추가해줬으니 명령어가 문제없이 작동하는지 아래 명령으로 갱신 테스트 해 봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;color: #212529;&quot;&gt;$ sudo certbot renew --dry-run&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ndash;dry-run 옵션을 붙이면 실제 갱신이 아니라 명령어 수행중 오류가 발생하는지 미리 확인 해 볼 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://signpen.net/blog/2513870&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://signpen.net/blog/2513870&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>443</category>
      <category>https</category>
      <category>nginx</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/132</guid>
      <comments>https://giron.tistory.com/132#entry132comment</comments>
      <pubDate>Thu, 28 Jul 2022 18:11:16 +0900</pubDate>
    </item>
    <item>
      <title>커서 기반 페이지네이션 적용기</title>
      <link>https://giron.tistory.com/131</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;무한 스크롤 구현을 요구받아서 처리하려고 찾아본 결과 커서 기반 페이지네이션이라는 키워드가 있어서 찾아 공부해봤다.&lt;/p&gt;
&lt;h2 id=&quot;span-stylecolor0b6e991-페이지네이션pagination-이란span&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;1. 페이지네이션(Pagination) 이란?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 데이터에서 지정된 개수만 데이터를 전달하는 방법&lt;/li&gt;
&lt;li&gt;필요한 데이터만 주고받으므로 네트워크의 오버헤드를 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;구현 방법에는 크게 두 가지가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;오프셋 기반 페이지네이션 (Offset-based Pagination)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커서 기반 페이지네이션 (Cursor-based Pagination)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;offset만큼 읽는데 이전의 읽었던 것을 다시 쭉 읽은 후 조회해서 데이터가 많아지면 성능상 안 좋다.&lt;/li&gt;
&lt;li&gt;데이터 중복 문제: 2페이지 끝까지 읽었는데 앞에 최신 데이터가 들어오면 3페이지 읽을 때 중복이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;JPA에서는 Pageable을 이용해서 쉽게 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOsWo9/btrIptdJrOn/xOiOT3hM5sR48LPghSQzK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOsWo9/btrIptdJrOn/xOiOT3hM5sR48LPghSQzK1/img.png&quot; data-alt=&quot;오프셋 페이징 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOsWo9/btrIptdJrOn/xOiOT3hM5sR48LPghSQzK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOsWo9%2FbtrIptdJrOn%2FxOiOT3hM5sR48LPghSQzK1%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;551&quot; height=&quot;64&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;오프셋 페이징 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;커서 &lt;/span&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;&lt;b&gt;offset을 사용하지 않&lt;/b&gt;고 Cursor를 기준으로 다음 n개의 데이터를 응답해주는 방식이다&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 Cursor가 unique 한 값이어야 한다. &lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정렬과 같은 기능을 사용할 때, pk값과 함께 사용해서 해결한다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 중복이 발생하지 않고, offset과 다르게 &lt;b&gt;이전의 데이터를 읽지 않고&lt;/b&gt; 바로 다음 cursor에 대한 정보를 주면 되므로 &lt;b&gt;대량의 데이터를 다룰 때 성능상 좋다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대신 where절에 여러 조건이 들어가면 성능이 offset보다 안 좋다고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 블로그에서 이야기해줬고 '페이스북이나 인스타그램도 그래서 정렬이 없구나'라고 생각했지만 유튜브에는 또 무한 스크롤로 정렬 기능이 있어서 더 알아봐야 할 것 같다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 기술엔 trade-off가 있듯이 커서기반페이지네이션의 단점이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp; where에 사용되는 기준 key가 중복이 가능할 경우이다. 이러면 정확한 값이 나오지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 요구 사항에 1페이지에서 바로 5페이지로 건너 뛰는 요구사항이 있으면 불가능 하다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cursor기반 코드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1659255644986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return queryFactory
                .selectFrom(article)
                .where(
                        ltarticleId(cursorId),
                        categoryEquals(category)
                )
                .limit(pageSize)
                .fetch();
}
private BooleanExpression ltarticleId(Long cursorId) {
        return cursorId == null ? null : article.id.lt(cursorId);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Offset기반 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1659255675404&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return queryFactory
                .selectFrom(article)
                .where(
                        categoryEquals(category)
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1만건 기준 &lt;b&gt;처음 게시물을 조회&lt;/b&gt;할 때와&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;9800번째를 조회&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;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10만건 기준으로 가보겠다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9만 8천번째 게시물들을 조회 할 때이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Offset 기반&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;247&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYKc04/btrIA1JcqmO/NJLTZ6vVOiXabdidceYSS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYKc04/btrIA1JcqmO/NJLTZ6vVOiXabdidceYSS1/img.png&quot; data-alt=&quot;ms단위&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYKc04/btrIA1JcqmO/NJLTZ6vVOiXabdidceYSS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYKc04%2FbtrIA1JcqmO%2FNJLTZ6vVOiXabdidceYSS1%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;247&quot; height=&quot;39&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ms단위&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor 기반&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;223&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baNDy4/btrIEcDbmJz/fZ0MoxR7WikEN8p4xtR870/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baNDy4/btrIEcDbmJz/fZ0MoxR7WikEN8p4xtR870/img.png&quot; data-alt=&quot;ms단위&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baNDy4/btrIEcDbmJz/fZ0MoxR7WikEN8p4xtR870/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaNDy4%2FbtrIEcDbmJz%2FfZ0MoxR7WikEN8p4xtR870%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;223&quot; height=&quot;39&quot; data-origin-width=&quot;223&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ms단위&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 타지 않는 이상 Cursor기반 페이지네이션은 &lt;span&gt;속도에서&lt;span&gt; &lt;/span&gt;&lt;/span&gt;의미가 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스를 통한 비교&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;더미데이터&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rdyqy/btrJTQ7teBl/rkP2wv3mK82UKrssyzkwD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rdyqy/btrJTQ7teBl/rkP2wv3mK82UKrssyzkwD0/img.png&quot; data-alt=&quot;더미 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rdyqy/btrJTQ7teBl/rkP2wv3mK82UKrssyzkwD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frdyqy%2FbtrJTQ7teBl%2FrkP2wv3mK82UKrssyzkwD0%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;1020&quot; height=&quot;731&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;731&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;더미 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;offset 페이징&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBxugK/btrJUZiM9rU/uYMsJemCJL0tkT9GgXIiz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBxugK/btrJUZiM9rU/uYMsJemCJL0tkT9GgXIiz0/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBxugK/btrJUZiM9rU/uYMsJemCJL0tkT9GgXIiz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBxugK%2FbtrJUZiM9rU%2FuYMsJemCJL0tkT9GgXIiz0%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;1194&quot; height=&quot;732&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;732&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset페이징을 하면 앞에서부터 쭉 보므로 18ms가 걸렸다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;No offset페이징&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XUqYd/btrJWOm3Kzr/aBYx2KjJhLxs6d33Q0eTc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XUqYd/btrJWOm3Kzr/aBYx2KjJhLxs6d33Q0eTc1/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XUqYd/btrJWOm3Kzr/aBYx2KjJhLxs6d33Q0eTc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXUqYd%2FbtrJWOm3Kzr%2FaBYx2KjJhLxs6d33Q0eTc1%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;1140&quot; height=&quot;728&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 no Offset은 27300번부터 10개의 페이징을 했을 때, 4ms의 시간이 걸리는 것을 확인 할 수 있었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;최신순 정렬&lt;/h4&gt;
&lt;pre id=&quot;code_1658883325259&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; @Override
    public List&amp;lt;Article&amp;gt; findAllByPage(Long cursorId, Integer cursorViews, String category, String sortType,
                                       int pageSize) {

        return queryFactory
                .selectFrom(article)
                .where(
                        ltArticleId(cursorId);
                        categoryEq(category))
                .limit(pageSize + 1)
                .orderBy(article.id.desc())
                .fetch();
    }

    private BooleanExpression ltArticleId(Long cursorId) {
        return cursorId == null ? null : article.id.lt(cursorId);
    }

    private BooleanExpression categoryEq(String category) {
        return &quot;all&quot;.equals(category) ? null : article.category.eq(Category.from(category));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 최신순으로 구현되어있기 때문에 마지막으로 조회한 게시물의 id가 cursorId가 된다. 예를 들어 200~195번까지 조회를 했으면 다음 페이지를 조회할 때는 195가 cursorId가 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 article.id.lt(195)가 되고, 195보다 작은 다음 id들이 해당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;size+1만큼 가져온 이유는 hasNext라는 변수명으로 페이징 이후에 조회할 게시물이 있는지 확인하려고 size보다 한 개 더 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 fetch().size()가 size+1과 같으면 hasNext는 true가 되고, 아니면 hasNext는 false가 된다.&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;현재는 DTO의 의존성을 repository에서 갖기 싫어서 Entity조회로 했지만 추후에 성능이 안 좋아진다고 판단이 되면 DTO로 조회해서 한 번에 끌어오게 할 수도 있을 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정렬 기준에 따라 (조회수 정렬 or 최신순 정렬)&lt;/h4&gt;
&lt;pre id=&quot;code_1658927537832&quot; class=&quot;lasso&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
    public List&amp;lt;Article&amp;gt; findAllByPage(Long cursorId, Integer cursorViews, String category,
                                       String sortType, int pageSize) {

        JPAQuery&amp;lt;Article&amp;gt; query = queryFactory
                .selectFrom(article)
                .where(
                        cursorIdAndCursorViews(cursorId, cursorViews, sortType),
                        categoryEq(category))
                .limit(pageSize + 1);

        if (sortType.equals(&quot;views&quot;)) {
            return query.orderBy(article.views.desc(), article.id.desc()).fetch();
        }
        return query.orderBy(article.id.desc()).fetch();
    }

    private BooleanExpression cursorIdAndCursorViews(Long cursorId, Integer cursorViews, String sortType) {
        if (sortType.equals(&quot;views&quot;)) {
            if (cursorId == null || cursorViews == null) {
                return null;
            }

            return article.views.eq(cursorViews)
                    .and(article.id.lt(cursorId))
                    .or(article.views.lt(cursorViews));
        }

        return ltArticleId(cursorId);
    }

    private BooleanExpression ltArticleId(Long cursorId) {
        return cursorId == null ? null : article.id.lt(cursorId);
    }

    private BooleanExpression categoryEq(String category) {
        return &quot;all&quot;.equals(category) ? null : article.category.eq(Category.from(category));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Slice를 사용한 hasNext&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 프론트엔드에서 페이지를 조회할 때, 다음 elements가 있는지 hasNext같은 변수로 가르쳐달라고 했다. 해당 요구사항을 충족시키기 위해서 JPA의 Slice와 Pageable을 사용해서 구현을 할 수있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Pageable을 받으면 무조건 offset을 사용한다고 생각해서 커서기반으로 페이지네이션을 하는 현재 상황에서 불필요하다고 생각했다. 하지만 Slice를 사용하려면 Pageable을 인자로 넣어줘야했었고 Slice를 사용하지 않고 구현을 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위의 코드처럼 List에서 limit(pageSize + 1)만큼 조회하고 service layer에서 분기처리를 통해 hasNext를 입력해주었다. 위처럼 작성하면 repository에서 pageSize만큼 데이터를 추출해주는 역할이 service layer로 넘어갔다는 것을 확인할수 있고, 이는 데이터의 응집도가 떨어진다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 hasNext를 repository내에서 판단하기위해 Pageable을 인자로 받고 Slice를 통해서 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 repository에서 바로 Dto로 반환을 한다면 Pageable이나 Slice를 의존하지 않고도 repository내에서 처리할 수 있다. 또한 영속성컨텍스트에 엔티티를 적재하지 않으므로&lt;b&gt; 불필요한 영속성컨텍스트 차지를 막고&lt;/b&gt; &lt;b&gt;엔티티의 사용하지 않는 컬럼까지 조회할 일이 없다&lt;/b&gt;.) 하지만 현재는 Article의 대부분이 Dto에도 들어가므로 엔티티를 조회하는 것이 Dto의 의존을 repository까지 들어오는 것 보단 이득이라고 생각이 되었다. 하지만 요구사항으로 like의 수도 반환하므로 현재처럼 하면 article조회 후, 다시 like를 조회하는 쿼리가 나가므로 추후 Dto를 반환하도록 리펙토링할 예정이다!&lt;/p&gt;
&lt;pre id=&quot;code_1660756459515&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Slice&amp;lt;Article&amp;gt; findAllByLikes(Long cursorId, Long cursorLikes, String category, Pageable pageable) {

        List&amp;lt;Article&amp;gt; fetch = queryFactory
                .select(article)
                .from(like)
                .rightJoin(like.article, article)
                .where(categoryEquals(category))
                .groupBy(article)
                .having(cursorIdAndLikes(cursorId, cursorLikes))
                .limit(pageable.getPageSize() + 1)
                .orderBy(like.count().desc(), article.id.desc())
                .fetch();

        boolean hasNext = false;

        if (fetch.size() == pageable.getPageSize() + 1) {
            fetch.remove(pageable.getPageSize());
            hasNext = true;
        }
        return new SliceImpl&amp;lt;&amp;gt;(fetch, pageable, hasNext);
    }


    private BooleanExpression cursorIdAndLikes(Long cursorId, Integer likes) {
        if (cursorId == null || likes == null) {
            return null;
        }
        return like.count().eq((long) likes)
                .and(article.id.lt(cursorId))
                .or(like.count().lt(likes));
    }
    
    private BooleanExpression categoryEq(String category) {
        return &quot;all&quot;.equals(category) ? null : article.category.eq(Category.from(category));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/528&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jojoldu.tistory.com/528&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@znftm97/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98Cursor-based-Pagination%EC%9D%B4%EB%9E%80-Querydsl%EB%A1%9C-%EA%B5%AC%ED%98%84%EA%B9%8C%EC%A7%80-so3v8mi2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@znftm97/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98Cursor-based-Pagination%EC%9D%B4%EB%9E%80-Querydsl%EB%A1%9C-%EA%B5%AC%ED%98%84%EA%B9%8C%EC%A7%80-so3v8mi2&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>CURSOR</category>
      <category>no offset</category>
      <category>pagenation</category>
      <category>Paging</category>
      <category>querydsl</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/131</guid>
      <comments>https://giron.tistory.com/131#entry131comment</comments>
      <pubDate>Wed, 27 Jul 2022 22:29:13 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] cascade = CascadeType.REMOVE와 @OnDelete(action = OnDeleteAction.CASCADE)의 차이</title>
      <link>https://giron.tistory.com/130</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 차이로는, JPA에 의해 처리되느냐, DDL에 의해 cascade가 걸린 테이블이 생겨 DB단에서 처리되느냐 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자의 방식을 취할 경우, JPA에 의해 외래 키를 찾아가며 참조하는 레코드를 제거해주게&lt;span&gt;&amp;nbsp;&lt;/span&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, JPA 상에서는 참조하고 있는 레코드의 개수만큼 delete 쿼리가 생성됩니다.&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;후자의 방식을 취할 경우, 데이터베이스 자체에서 on delete cascade 제약조건이 걸리게 됩니다. 이를 통해 참조하는 레코드가 모두 제거되는 것입니다.&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;192&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XSRbE/btrHJKIXQ6e/kMGxECrOoZY6skQBdCTzVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XSRbE/btrHJKIXQ6e/kMGxECrOoZY6skQBdCTzVK/img.png&quot; data-alt=&quot;delete article&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XSRbE/btrHJKIXQ6e/kMGxECrOoZY6skQBdCTzVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXSRbE%2FbtrHJKIXQ6e%2FkMGxECrOoZY6skQBdCTzVK%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;192&quot; height=&quot;173&quot; data-origin-width=&quot;192&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;delete article&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;cascade = CASCADE.REMOVE&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA 레벨에서 작동한다.&lt;/li&gt;
&lt;li&gt;외래 키를 찾아 참조하는 레코드를 제거해준다.&lt;/li&gt;
&lt;li&gt;참조하고 있는 레코드 수 만큼 delete쿼리가 나간다.&lt;/li&gt;
&lt;li&gt;부모 테이블의 자식 컬럼에 걸어여하므로(일반적으로 @OneToMany(cascade= CASCADE.REMOVE) &lt;b&gt;양방향 매핑이 아닌 이상 적용하기 어렵다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@OnDelete&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;database에서 직접 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;DDL 생성시 cascade 제약 조건이 생성 됨.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;데이터베이스 자체에서 on delete cascade 제약조건이 걸리게 된다.&lt;/li&gt;
&lt;li&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;대충 @OnDelete가 좋아보이는데,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;@OnDelete 방식으로 테이블을 생성하여 데이터베이스를 직접 다루다보면, on delete cascade에 의해 어떠한 레코드의 참조 레코드까지 연쇄적으로 삭제해버리는 실수 할 여지가 있는 반면,&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 한다. 해당 부분은 이해가 잘 안가서 다음에 봐야할 것 같다.&lt;/p&gt;</description>
      <category>우아한테크코스 4기/프로젝트</category>
      <category>ddl</category>
      <category>JPA</category>
      <category>키 제약 조건</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/130</guid>
      <comments>https://giron.tistory.com/130#entry130comment</comments>
      <pubDate>Sun, 24 Jul 2022 01:13:33 +0900</pubDate>
    </item>
    <item>
      <title>[프록시] 스프링에서 사용되는 proxy전략</title>
      <link>https://giron.tistory.com/129</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링과 JPA로 애플리케이션을 개발하다 보면 프록시에 대해서 많이 만나게 된다. 이와 관련되어 구글링을 해보면 게시글들마다 다른 말을 해서 직접 정리해보려고 한다. - 총정리&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;1590&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BeRio/btrH33mht9C/j8qDk4CXci1ReK5vQKM6ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BeRio/btrH33mht9C/j8qDk4CXci1ReK5vQKM6ok/img.png&quot; data-alt=&quot;프록시란&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BeRio/btrH33mht9C/j8qDk4CXci1ReK5vQKM6ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBeRio%2FbtrH33mht9C%2Fj8qDk4CXci1ReK5vQKM6ok%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;1590&quot; height=&quot;329&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프록시란&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링&amp;nbsp;AOP는&amp;nbsp;[런타임에&amp;nbsp;프록시&amp;nbsp;인스턴스가&amp;nbsp;동적으로&amp;nbsp;변경되는]&amp;nbsp;다이나믹&amp;nbsp;프록시&amp;nbsp;기법으로&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;756&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qa8dO/btrHZ7cCCjG/nxZsH7k5SlTNn8vYbO5zWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qa8dO/btrHZ7cCCjG/nxZsH7k5SlTNn8vYbO5zWk/img.png&quot; data-alt=&quot;스프링에서 프록시 생성 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qa8dO/btrHZ7cCCjG/nxZsH7k5SlTNn8vYbO5zWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqa8dO%2FbtrHZ7cCCjG%2FnxZsH7k5SlTNn8vYbO5zWk%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;756&quot; height=&quot;375&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스프링에서 프록시 생성 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;인터페이스의 유무에 따라 다음과 같이 나뉜다.&lt;br /&gt;1.&amp;nbsp;JDK&amp;nbsp;다이나믹&amp;nbsp;프록시 &lt;br /&gt;2.&amp;nbsp;CGLIB&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;JDK Dynamic Proxy&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;JDK에서 지원하는 프록시 생성 방법&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Invocation Handler를 재정의한 &lt;b&gt;invoke&lt;/b&gt;를 구현하여 부가기능 수행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Reflection API 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인터페이스를 통해서만 프록시 생성 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;spring에서 default
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시될 대상 객체가 하나 이상의 인터페이스를 구현하는 경우, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;JDK Dynamic Proxy가 사용되며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;대상 유형이 구현한 모든 인터페이스가 프록시됩니다.&lt;/li&gt;
&lt;li&gt;대상 객체가 인터페이스를 구현하지 않는 경우, 런타임에 대상 유형의 하위 클래스인 CGLIB 프록시가 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CodeGenratorLibrary(CGLIB)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상속을 기반으로 프록시 생성&lt;/li&gt;
&lt;li&gt;final이 붙으면 오버라이딩이 불가능하므로 CGLIB프록시가 작동하지 않는다.&lt;/li&gt;
&lt;li&gt;Enhancer의존성을 추가해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3.2 version Spring core 패키지에 포함되어서 의존성 추가하지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Default 생성자가 필요하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4.0 version Objensis 라이브러리를 포함하면서 필요 없어졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;타겟의 생성자를 두 번 호출한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4.0 version Objensis 라이브러리를 포함하면서 한 번만 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;spring boot 1.4부터 default로 CGLIB을 사용하게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실제 우리가 스프링부트에서 인터페이스를 만들고 Proxy를 적용하려고 해도 CGLIB이 적용된다. 인터페이스를 적용했을 때, JDK 동적 프록시를 사용하려면 properties혹은 yml파일에서 proxy-target-class를 false로 바꿔주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eBbJAJ/btrH1BKPqRj/jHMaRtUE5VREs7P4phgcm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eBbJAJ/btrH1BKPqRj/jHMaRtUE5VREs7P4phgcm0/img.png&quot; data-alt=&quot;yml&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eBbJAJ/btrH1BKPqRj/jHMaRtUE5VREs7P4phgcm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeBbJAJ%2FbtrH1BKPqRj%2FjHMaRtUE5VREs7P4phgcm0%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;654&quot; height=&quot;274&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;yml&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xKD9r/btrHJAkWRWp/XKbxNnNiYp4oX4kLUX3ux0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xKD9r/btrHJAkWRWp/XKbxNnNiYp4oX4kLUX3ux0/img.png&quot; data-alt=&quot;더 빠르다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xKD9r/btrHJAkWRWp/XKbxNnNiYp4oX4kLUX3ux0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxKD9r%2FbtrHJAkWRWp%2FXKbxNnNiYp4oX4kLUX3ux0%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;1125&quot; height=&quot;546&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;더 빠르다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reflection api는 C코드로 실행한다. 그래서 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGLIB을 사용할 때의 FastClass는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;JVM 내에서 직접 메서드를 호출하는&lt;span&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;바이트 코드&lt;/b&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;또한 JDK dynamic proxy는 reflection api를 사용하기 때문에 성능상 느리다.(JVM에 optimization이 작동하지 않으므로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 CGLIB은 바이트 코드를 사용하므로 성능이 빠르다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1357&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5EUPF/btrH31B0Cy7/9eDofObd3RqEIHf6aCw2xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5EUPF/btrH31B0Cy7/9eDofObd3RqEIHf6aCw2xK/img.png&quot; data-alt=&quot;성능 표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5EUPF/btrH31B0Cy7/9eDofObd3RqEIHf6aCw2xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5EUPF%2FbtrH31B0Cy7%2F9eDofObd3RqEIHf6aCw2xK%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;1357&quot; height=&quot;599&quot; data-origin-width=&quot;1357&quot; data-origin-height=&quot;599&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성능 표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굵은 글씨가 유의미한 지표라고 한다. 보면은 CGLIB이 JDK Dynamic 프록시보다 3배 가까이 성능이 좋다는 것을 알 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 스프링 부트 2.0부터는 default로 CGLIB proxying을 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스를 사용하여 DI를 적극 활용하는 방식을 두고 왜 CGLIB을 채택했을 까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdClqH/btrHPKHvg3v/T1OhMsUtkBWKBk18xeTHe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdClqH/btrHPKHvg3v/T1OhMsUtkBWKBk18xeTHe0/img.png&quot; data-alt=&quot;스프링부트 총 책임자 phill&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdClqH/btrHPKHvg3v/T1OhMsUtkBWKBk18xeTHe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdClqH%2FbtrHPKHvg3v%2FT1OhMsUtkBWKBk18xeTHe0%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;666&quot; height=&quot;233&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;233&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스프링부트 총 책임자 phill&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;phil의 말에 따르면 아래와 같다고 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;인터페이스 기반 프록시는 때때로 ClassCast Exceptions를 추적하기 어렵게 한다. 특히, @Bean이 JDK 프록시로 대체되어 원래 클래스 형식으로 주입되지 않을 수 있었다.&lt;br /&gt;&lt;br /&gt;스프링 프레임워크는 요즘 CGLIB의 음영 복사가 있기 때문에, 즉시 사용하지 않을 이유가 거의 없었다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음영 복사에 대해서는 더 공부해봐야겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프록시 팩토리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;다이나믹 프록시를 생성한다&lt;/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;팩토리 빈의 구현체&lt;/b&gt;인 &lt;b&gt;프록시 팩토리 빈&lt;/b&gt; 사용한다. 팩토리 빈의 구현체이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부가기능을&amp;nbsp;invocationHandler가&amp;nbsp;아닌&amp;nbsp;addAdvice()를&amp;nbsp;통해&amp;nbsp;구현한다.&lt;/li&gt;
&lt;li&gt;Proxy&amp;nbsp;Factory&amp;nbsp;Bean은&amp;nbsp;인터페이스가&amp;nbsp;없어도&amp;nbsp;클래스를&amp;nbsp;받아서&amp;nbsp;인터페이스를&amp;nbsp;검출하여&amp;nbsp;프록시를&amp;nbsp;생성할&amp;nbsp;수&amp;nbsp;있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서 프록시가 타깃 오브젝트에 의존하지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 팩토리 빈의 MethodInterceptor의 invoke메소드는 프록시 팩토리 빈으로부터 타겟에 대한 정보도 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MethodInterceptor가 타겟에 의존하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 methodInterceptor 오브젝트는 타겟이 다른 여러 프록시에서 함께 사용할수 있고 싱글톤으로 빈 등록이 가능하다.&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;rarr; 자동 프록시 생성기&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;프록시 자체는 aop가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시를 만들면 빈 이름이 같은 게 만들어지는데 Quailfier를 안쓰고 이를 해결하는 방식이 &lt;b&gt;자동 프록시 생성기&lt;/b&gt;가 컨테이너 초기화 때 만들어진 빈을 바꿔치기해서 프록시 빈을 자동 등록해준다. 이때 빈의 의존관계를 바꿔치기 하는 것은 빈 후처리기를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 프록시 생성기&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동 프록시 생성기 방식은 타겟을 빈으로 직접 노출되지 않는다!!&lt;/li&gt;
&lt;li&gt;aop적용 때문에 @Autowired를 사용하지 못하는 불상사를 막는다.&lt;/li&gt;
&lt;li&gt;proxy를 빈으로 등록하는 게 아닌 기존의 빈을 프록시 빈으로 후처리기를 통해 변경하여 저장하여 Qualifier를 적용할 필요가 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프록시 기반 aop는 자동 프록시 생성기를 통해 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에선 팩토리 빈의 구현체인 프록시 팩토리 빈 사용 -&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩토리 빈의 구현체이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시가 타깃 오브젝트에 의존하지 않아도 된다.&lt;/li&gt;
&lt;li&gt;부가기능은 addAdvice()로 구현한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 팩토리 빈 &amp;rarr; 자동 프록시 생성기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프록시 기반 aop는 자동 프록시 생성기를 통해 동작한다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK&amp;nbsp;다이내믹&amp;nbsp;프록시는&amp;nbsp;일반적인&amp;nbsp;방법으로&amp;nbsp;Spring의&amp;nbsp;Bean으로&amp;nbsp;등록할&amp;nbsp;수&amp;nbsp;없다 &lt;br /&gt;-&amp;gt;&amp;nbsp;왜냐하면&amp;nbsp;동적으로&amp;nbsp;프록시&amp;nbsp;객체를&amp;nbsp;알게 되므로&amp;nbsp;그&amp;nbsp;전에는&amp;nbsp;빈으로&amp;nbsp;등록&amp;nbsp;불가능 &lt;br /&gt;-&amp;gt;&amp;nbsp;팩토리&amp;nbsp;빈&amp;nbsp;등장&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;팩토리 빈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FactoryBean&amp;nbsp;인터페이스의&amp;nbsp;getObject()&amp;nbsp;메소드는&amp;nbsp;Bean&amp;nbsp;객체를&amp;nbsp;생성하는&amp;nbsp;목적을&amp;nbsp;가지고&amp;nbsp;있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 FactoryBean의 구현 클래스를 구성하고 기존에 작성했던 Proxy.newInstance()의 로직(JDK Dynamic Proxy 구현 메서드)을 getObject() 메소드에 작성해주면 FactoryBean에 의해 프록시 객체가 Bean으로 생성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1658567918566&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface FactoryBean&amp;lt;T&amp;gt; {
    T getObject() throws Exception;  &amp;rarr; Bean 객체를 생성하고 반환
    Class&amp;lt;?&amp;gt; getObjectType();  &amp;rarr; FactoryBean에 의해 생성된 객체의 Type
    default boolean isSingleton() {return true;}   &amp;rarr; getObject()의 반환된 객체의 싱글톤 여부
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Factory&amp;nbsp;Bean&amp;nbsp;구현&lt;/p&gt;
&lt;pre id=&quot;code_1658567941207&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MonitorFactoryBean implements FactoryBean&amp;lt;Object&amp;gt;{
    private Class&amp;lt;?&amp;gt; interfaces;
    private Object   target;

    @Override
    public Object getObject() throws Exception {
        return Proxy.newProxyInstance(getClass().getClassLoader()
                                    , new Class[] {interfaces}
                                    , new MonitorHandler(target));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프록시&amp;nbsp;패턴&amp;nbsp;-&amp;nbsp;접근방법&amp;nbsp;제어 &lt;br /&gt;데코레이터&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;프록시 방식의 AOP는 객체지향 디자인패턴의 데코레이터 패턴 또는 프록시 패턴을 응용해서 기존 코드에 영향 주지않는채로 부가기능을 제공할수 있는 oop에서 출발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 포인트 컷이라는 적용 대상 선택과 자동 프록시 생성이라는 적용까지 접목하면 aop가 된다.&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;br /&gt;그렇다면 엔티티에서는 왜 기본 생성자가 필요할까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8FWeo/btrEXPTFYGW/U81UWkYHv9PZAFY2nIKTX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8FWeo/btrEXPTFYGW/U81UWkYHv9PZAFY2nIKTX1/img.png&quot; data-alt=&quot;하이버네이트 공식 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8FWeo/btrEXPTFYGW/U81UWkYHv9PZAFY2nIKTX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8FWeo%2FbtrEXPTFYGW%2FU81UWkYHv9PZAFY2nIKTX1%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;1257&quot; height=&quot;397&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하이버네이트 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이버네이트 공식 문서를 보면 &quot;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;Hibernate는 Java Reflection을 사용하여 객체를 생성해야 합니다.&quot;라고 되어 있다. 즉 AOP는 &lt;/span&gt;CGLIB를 통해 상속을 받아 프록시 객체가 만들어지고 Hibernate의 Entity에서 프록시가 만들어질 때는 reflection ApI를 이용한다. 따라서 엔티티의 프록시에서 리플렉션을 사용해야 하므로 기본 생성자는 필요하다.&lt;/p&gt;
&lt;figure id=&quot;og_1658227561896&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;cglib: The missing manual&quot; data-og-description=&quot;The byte code instrumentation library cglib is a popular choice among many well-known Java frameworks such as Hibernate ( not anymore ...&quot; data-og-host=&quot;mydailyjava.blogspot.com&quot; data-og-source-url=&quot;http://mydailyjava.blogspot.com/2013/11/cglib-missing-manual.html&quot; data-og-url=&quot;http://mydailyjava.blogspot.com/2013/11/cglib-missing-manual.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;http://mydailyjava.blogspot.com/2013/11/cglib-missing-manual.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://mydailyjava.blogspot.com/2013/11/cglib-missing-manual.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;cglib: The missing manual&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The byte code instrumentation library cglib is a popular choice among many well-known Java frameworks such as Hibernate ( not anymore ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mydailyjava.blogspot.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;Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.jboss.org/docs/DOC-15785&quot;&gt;https://developer.jboss.org/docs/DOC-15785&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/questions/105043&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.inflearn.com/questions/105043&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pfb-proxy-types&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pfb-proxy-types&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/&lt;/a&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.1/quickstart/html_single/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.jboss.org/hibernate/orm/6.1/quickstart/html_single/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1655383731659&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Reflection API 간단히 알아보자.&quot; data-og-description=&quot;Spring Framework를 학습하다 보면 Java Reflection API를 자주 접하게 된다. 하지만 Reflection API&amp;hellip;&quot; data-og-host=&quot;tecoble.techcourse.co.kr&quot; data-og-source-url=&quot;https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/&quot; data-og-url=&quot;https://post/2020-07-16-reflection-api/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/DZZ12/hyOMG6cnti/7CvmjVcwsLJLptoGkkNyQK/img.png?width=2542&amp;amp;height=1414&amp;amp;face=0_0_2542_1414&quot;&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/DZZ12/hyOMG6cnti/7CvmjVcwsLJLptoGkkNyQK/img.png?width=2542&amp;amp;height=1414&amp;amp;face=0_0_2542_1414');&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;Reflection API 간단히 알아보자.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Framework를 학습하다 보면 Java Reflection API를 자주 접하게 된다. 하지만 Reflection API&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tecoble.techcourse.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1655374222438&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;기본 생성자에 관해 질문드립니다. - 인프런 | 질문 &amp;amp; 답변&quot; data-og-description=&quot;13:43 부분을 보면 JPA 는 내부적으로 리플렉션을 통해서 객체를 생성하기 때문에 기본 생성자가 필요하다고 하셨는데요. 다음과 같이 기본 생성자는 생성하지 않고 코드를 돌리더라도&amp;nbsp;문제없이 &quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/questions/105043&quot; data-og-url=&quot;https://www.inflearn.com/questions/105043&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dz5Osq/hyOMFzjRww/nhonf5DmyjazQKLErVfpj0/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=751_416_794_464,https://scrap.kakaocdn.net/dn/bHyiqN/hyOMDBxn47/zOHv69kXwnBUyGiXS8dAs1/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=751_416_794_464&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/questions/105043&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/questions/105043&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dz5Osq/hyOMFzjRww/nhonf5DmyjazQKLErVfpj0/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=751_416_794_464,https://scrap.kakaocdn.net/dn/bHyiqN/hyOMDBxn47/zOHv69kXwnBUyGiXS8dAs1/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=751_416_794_464');&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;기본 생성자에 관해 질문드립니다. - 인프런 | 질문 &amp;amp; 답변&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;13:43 부분을 보면 JPA 는 내부적으로 리플렉션을 통해서 객체를 생성하기 때문에 기본 생성자가 필요하다고 하셨는데요. 다음과 같이 기본 생성자는 생성하지 않고 코드를 돌리더라도&amp;nbsp;문제없이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&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>백앤드 개발일지/스프링부트</category>
      <category>AOP</category>
      <category>cglib</category>
      <category>JDK프록시</category>
      <category>우아한테크코스</category>
      <category>프록시</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/129</guid>
      <comments>https://giron.tistory.com/129#entry129comment</comments>
      <pubDate>Sat, 23 Jul 2022 18:42:34 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] hibernate.ddl-auto 설정</title>
      <link>https://giron.tistory.com/128</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;hibernate.ddl-auto 옵션은 Entity객체를 참고하여 애플리케이션 실행 시점에 하이버네이트에서 자동으로 DDL을 만들어주는 옵션이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 테스트 도중 fk 제약조건이 걸린 엔티티를 생성할 때, 아래 설정처럼 create를 하니 에러가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;28&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LFyxp/btrHbVihQ9n/gauk2xLaTRlnx9VNkHBDC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LFyxp/btrHbVihQ9n/gauk2xLaTRlnx9VNkHBDC1/img.png&quot; data-alt=&quot;properties&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LFyxp/btrHbVihQ9n/gauk2xLaTRlnx9VNkHBDC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLFyxp%2FbtrHbVihQ9n%2Fgauk2xLaTRlnx9VNkHBDC1%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;371&quot; height=&quot;28&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;28&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;properties&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1NBn6/btrHaX8vfum/Cx2fTkC7nT78oZOg4yz6hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1NBn6/btrHaX8vfum/Cx2fTkC7nT78oZOg4yz6hk/img.png&quot; data-alt=&quot;Table &amp;quot;ANSWER&amp;quot; not found;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1NBn6/btrHaX8vfum/Cx2fTkC7nT78oZOg4yz6hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1NBn6%2FbtrHaX8vfum%2FCx2fTkC7nT78oZOg4yz6hk%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;1758&quot; height=&quot;279&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Table &quot;ANSWER&quot; not found;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;ddl-auto-옵션-종류&quot; data-ke-size=&quot;size26&quot;&gt;ddl-auto 옵션&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;create: 기존테이블 삭제 후 다시 생성 + 닫을 때 삭제하지 않는다.&lt;/li&gt;
&lt;li&gt;create-drop: create와 같으나 종료시점에 테이블 DROP&lt;/li&gt;
&lt;li&gt;update: 하이버네이트는 주어진 엔티티 구조에 따라서 데이터베이스를 변경한다.&lt;/li&gt;
&lt;li&gt;validate: 엔티티와 테이블이 정상 매핑되었는지만 확인&lt;/li&gt;
&lt;li&gt;none: 어떠한 변화도 주지 않는다.mysql에서 default이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ddl-auto의 옵션의 시작은&amp;nbsp;&lt;b&gt;create&lt;/b&gt; or&amp;nbsp;&lt;b&gt;update&lt;/b&gt;로 시작해야한다. 왜냐하면 처음에는 테이블의 스키마가 없기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;The&amp;nbsp;spring.jpa.hibernate.ddl-auto&amp;nbsp;is a special case, because, depending on runtime conditions, it has different defaults. If an embedded database is used and no schema manager (such as Liquibase or Flyway) is handling the&amp;nbsp;DataSource, it defaults to&amp;nbsp;create-drop. In all other cases, it defaults to&amp;nbsp;none.&lt;br /&gt;The dialect to use is detected by the JPA provider. If you prefer to set the dialect yourself, set the spring.jpa.database-platform property.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내장된 db가 사용&lt;/b&gt;되거나 &lt;b&gt;flyway와 같은 스키마 manager가 없으면&lt;/b&gt; &lt;b&gt;default로 create-drop&lt;/b&gt;으로 지정이 된다. ex)h2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 모든 경우에는&lt;b&gt; none&lt;/b&gt;이 적용된다고 한다.ex)mysql&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CommandAcceptanceException: Error executing DDL &quot; alter table {table} drop foreign key ~&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 해결 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create나 create-drop을 하면 테이블을 drop을 하고 create를 한다. 이때 테이블을 먼저 drop을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 table을 create하기 전에 alter table 구문으로 foreign key를 생성하는데 이미 테이블이 제거되어서 테이블이 없다고 에러가 발생한다.&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;따라서 ddl-auto : update로 변경하면 변경 부분만 반영되므로 해결이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spring.io/guides/gs/accessing-data-mysql/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://spring.io/guides/gs/accessing-data-mysql&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <category>ddl-auto</category>
      <category>Hibernate</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/128</guid>
      <comments>https://giron.tistory.com/128#entry128comment</comments>
      <pubDate>Thu, 14 Jul 2022 00:32:27 +0900</pubDate>
    </item>
    <item>
      <title>[JPA]EnableJpaAuditing을 Application 위에 쓰면 안되는 이유</title>
      <link>https://giron.tistory.com/127</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;@WebMvcTest를 붙이고 테스트를 돌리니 &lt;b&gt;JPA metamodel must not be empty! &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;이유를 찾아보니 테스트를 돌릴때는 기본적으로 XApplication이 돌면서 작동한다. 따라서 @EnableJpaAuditing을 Application위에 올리면 Jpa관련된 빈들이 올라오기를 요구한다. 이때, mockMvc를 사용한 테스트는 mvc와 관련한 빈들만 찾아서 올리므로 JPA관련 빈을 찾지 못해서 &lt;b&gt;JPA metamodel must not be empty&amp;nbsp;&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;/h3&gt;
&lt;h3 id=&quot;31-configuration-분리&quot; data-ke-size=&quot;size23&quot;&gt;1. @Configuration 분리&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaAuditingConfig.java&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@EnableJpaAuditing
@Configuration
public class JpaAuditingConfig {  
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;32-mockbean-추가&quot; data-ke-size=&quot;size23&quot;&gt;2. @MockBean 추가&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebMvcTest.java&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RunWith(SpringRunner.class)
@WebMvcTest(TargetController.java)
@MockBean(JpaMetamodelMappingContext.class)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 클래스에 JpaMetamodelMappingContext를 MockBean으로 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번처럼 매번 WebMvcTest마다 @MockBean을 추가해주는 방법보다는 1번 처럼 Configuration을 분리해주는 방식이 더 편한 것 같다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 해결방법을 사용하면 모든게 해결이 될 줄알았다. 그런데 @DataJpaTest를 사용할 때 문제가 발생했다.&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;상황은 createdAt을 가지고 있는 Article 클래스가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qBJlU/btrHfmUETSX/YSkKm7GUUv6CepqnFZSbh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qBJlU/btrHfmUETSX/YSkKm7GUUv6CepqnFZSbh0/img.png&quot; data-alt=&quot;@DataJpa를 이용한 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qBJlU/btrHfmUETSX/YSkKm7GUUv6CepqnFZSbh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqBJlU%2FbtrHfmUETSX%2FYSkKm7GUUv6CepqnFZSbh0%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;742&quot; height=&quot;423&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;423&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@DataJpa를 이용한 테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfgtD/btrHdxQFn2F/4i8tKdwkpU6kxEUXEyRk31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfgtD/btrHdxQFn2F/4i8tKdwkpU6kxEUXEyRk31/img.png&quot; data-alt=&quot;createdAt이 null이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfgtD/btrHdxQFn2F/4i8tKdwkpU6kxEUXEyRk31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfgtD%2FbtrHdxQFn2F%2F4i8tKdwkpU6kxEUXEyRk31%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;802&quot; height=&quot;186&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;createdAt이 null이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save 후에도 null이 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;@DataJpaTest&lt;/b&gt;는 JpaTest에 필요한 최소한의 빈을 불러오는데, 거기에는 @Configuration 빈이 포함되어있지 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 @Configuration을 사용하여 따로 설정 파일을 만들었을 경우 아래 사진처럼 Import(JpaAuditingConfig.class)를 넣어줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A0dun/btrHfnF114C/D628VKtHAJiHMQ1t9h9hU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A0dun/btrHfnF114C/D628VKtHAJiHMQ1t9h9hU0/img.png&quot; data-alt=&quot;import&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A0dun/btrHfnF114C/D628VKtHAJiHMQ1t9h9hU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA0dun%2FbtrHfnF114C%2FD628VKtHAJiHMQ1t9h9hU0%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;336&quot; height=&quot;83&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;import&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://xlffm3.github.io/spring%20&amp;amp;%20spring%20boot/JPAError/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://xlffm3.github.io/spring%20&amp;amp;%20spring%20boot/JPAError/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1657608229347&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Boot 테스트 에러 : JPA metamodel must not be empty!&quot; data-og-description=&quot;슬라이스 테스트를 진행할 때 의존성 주입에 신경쓰자.&quot; data-og-host=&quot;xlffm3.github.io&quot; data-og-source-url=&quot;https://xlffm3.github.io/spring%20&amp;amp;%20spring%20boot/JPAError/&quot; data-og-url=&quot;https://xlffm3.github.io/spring%20&amp;amp;%20spring%20boot/JPAError/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ckYygC/hyO3tMvd5U/VKojaevSTg0tX3lyxNCGk1/img.jpg?width=1600&amp;amp;height=1064&amp;amp;face=0_0_1600_1064&quot;&gt;&lt;a href=&quot;https://xlffm3.github.io/spring%20&amp;amp;%20spring%20boot/JPAError/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://xlffm3.github.io/spring%20&amp;amp;%20spring%20boot/JPAError/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ckYygC/hyO3tMvd5U/VKojaevSTg0tX3lyxNCGk1/img.jpg?width=1600&amp;amp;height=1064&amp;amp;face=0_0_1600_1064');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 테스트 에러 : JPA metamodel must not be empty!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;슬라이스 테스트를 진행할 때 의존성 주입에 신경쓰자.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;xlffm3.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <category>@EnableJpaAuditing</category>
      <category>WebMvc</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/127</guid>
      <comments>https://giron.tistory.com/127#entry127comment</comments>
      <pubDate>Tue, 12 Jul 2022 15:46:10 +0900</pubDate>
    </item>
    <item>
      <title>[h2]DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;</title>
      <link>https://giron.tistory.com/126</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;제이슨의 JPA수업 도중 application.properties의 설정에서 아래와 같은 h2 설정이 있었다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.datasource.url=jdbc:h2:~/test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2설정할 때, 어쩔때는 h2:mem, h2:tcp였고 이번에는 그냥 h2:~였다. 또한 DB_CLOSE~라는 새로운 이름도 보여서 h2에 대해서 조금 공부해보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Connection Modes&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Embedded mode (local connections using JDBC)&lt;/li&gt;
&lt;li&gt;Server mode (remote connections using JDBC or ODBC over TCP/IP)&lt;/li&gt;
&lt;li&gt;Mixed mode (local and remote connections at the same time)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 3가지중 자주 사용하는 Embedded와 Server모드에 대해서만 알아보겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Embedded mode&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;307&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmCPUP/btrGYzT0lqP/gKywsJGS4iPfLTj63WmPjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmCPUP/btrGYzT0lqP/gKywsJGS4iPfLTj63WmPjk/img.png&quot; data-alt=&quot;메인메모리 속의 JVM(애플리케이션)속에 h2가 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmCPUP/btrGYzT0lqP/gKywsJGS4iPfLTj63WmPjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmCPUP%2FbtrGYzT0lqP%2FgKywsJGS4iPfLTj63WmPjk%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;307&quot; height=&quot;353&quot; data-origin-width=&quot;307&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메인메모리 속의 JVM(애플리케이션)속에 h2가 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;임베디드 모드에서는 애플리케이션이 JDBC를 사용하여 &lt;b&gt;동일한 JVM 내에서 데이터베이스를 엽니다&lt;/b&gt;. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;장점으로는 &lt;b&gt;가장 빠르고 쉬운 연결 모드&lt;/b&gt;이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;단점은 데이터베이스가 한 번에 하나의 virtual machine(및 클래스 로더)에서만 열릴 수 있다는 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;I/O 작업은 &lt;b&gt;SQL 명령을 실행하는 응용 프로그램의 스레드에 의해 수행&lt;/b&gt;될 수 있다. 애플리케이션이 이러한 스레드를 중단하지 않을 수 있으며, 스레드 중단 중에 JVM이 I/O 핸들을 닫기 때문에 &lt;b&gt;데이터베이스가 손상될 수 있다&lt;/b&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Server Mode&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5pIvP/btrGWyOPh5Y/GIy3Q2uZBD4XycgGZuJWJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5pIvP/btrGWyOPh5Y/GIy3Q2uZBD4XycgGZuJWJ1/img.png&quot; data-alt=&quot;애플리케이션과 다른 디비 프로세스를 열어서 연결한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5pIvP/btrGWyOPh5Y/GIy3Q2uZBD4XycgGZuJWJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5pIvP%2FbtrGWyOPh5Y%2FGIy3Q2uZBD4XycgGZuJWJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;290&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;애플리케이션과 다른 디비 프로세스를 열어서 연결한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;서버 모드를 사용할 때 응용 프로그램은 JDBC 또는 ODBC API를 사용하여 원격으로 데이터베이스를 연다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;b&gt;많은 응용프로그램이&lt;/b&gt; 이 서버에 연결하여 &lt;b&gt;동일한 데이터베이스에 동시에 연결&lt;/b&gt;할 수 있습니다. 내부적으로 서버 프로세스는 데이터베이스를 임베디드 모드로 엽니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;모든 데이터가 TCP/IP를 통해 전송되기 때문에 서버 모드는 &lt;b&gt;내장 모드보다 느리다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;embedded_databases&quot; data-ke-size=&quot;size26&quot;&gt;Connecting to an Embedded (Local) Database&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;jdbc:h2:[file:][&amp;lt;path&amp;gt;]&amp;lt;databaseName&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;jdbc:h2:~/test&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;jdbc:h2:file:/data/sample&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;jdbc:h2:file:C:/data/sample (Windows only)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;와 같은 url 포멧일 때, 사용된다.&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: #000000;&quot;&gt;[file:]은 option으로 &lt;b&gt;안 적거나 상대경로를 적어야 한다. path&lt;/b&gt;와&lt;b&gt; databaseName은&lt;/b&gt; os마다 다르지만 &lt;b&gt;소문자로 적는 것을 공식문서에서 추천한다. databaseName은 3글자 이상이어야 하고 &lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;이름에는 세미콜론을 사용할 수 없다. &lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;사용자 홈 디렉토리를 가리키려면 다음과 같이 ~/를 사용한다. jdbc:h2:~/test.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;in_memory_databases&quot; data-ke-size=&quot;size23&quot;&gt;In-Memory Databases&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;b&gt;신속한 프로토타이핑, 테스트, 고성능 운영, 읽기 전용 데이터베이스&lt;/b&gt;와 같이 &lt;b&gt;데&lt;/b&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;b&gt;이터를 유지하거나 데이터를 변경하지 않아도 될때 사용하는게 좋다.&lt;/b&gt; 인메모리 디비는 &lt;b&gt;데이터가 지속되지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;데&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;이터베이스 URL은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ece9d8;&quot;&gt;jdbc:h2:mem:&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 통해서 새로운 커넥션으로 새로운 (private)디비를 생성할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;하나의 디비에 다중 연결이 필요할 때는 &lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ece9d8;&quot;&gt;jdbc:h2:mem:db1&lt;/span&gt;와 같이 이름을 붙여서 사용할 수 있다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;또 다른 컴퓨터에서 tcp를 통해 접근하려면 &lt;span style=&quot;background-color: #ece9d8; color: #000000;&quot;&gt;jdbc:h2:tcp://localhost/mem:db1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&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;background-color: #fdfdfd; color: #000000;&quot;&gt;기본적으로 데이터베이스에 대한 마지막 연결을 닫으면 데이터베이스가 닫힌다. 인메모리 디비에서는 내용이 사라진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;b&gt;데이터베이스를 열린 상태로 유지하려면&lt;/b&gt; 데이터베이스 URL에&lt;span style=&quot;background-color: #ece9d8; color: #000000;&quot;&gt;&lt;b&gt;;DB_CLOSE_DELAY=-1&lt;/b&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;background-color: #fdfdfd; color: #000000;&quot;&gt;즉, jvm이 살아있는 동안 메모리 내 데이터베이스의 내용을 유지하려면 jdbc:h2:mem:test;DB_CLOSE_DELay=-1을 하자.대신 &lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;이때 SHUTDOWN 명령을 사용하여 DB를 닫을때는 메모리 누수가 발생할 수 있다고 한다.(그런데 DB에 대한 마지막 커넥션이 닫히면 DB가 자동으로 닫히므로 SHUTDOWN명령어를 쓸 일은 없다고 보면 된다.)&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;;DB_CLOSE_ON_EXIT=FALSE(&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;VM 종료 시 데이터베이스 닫기 안 함)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;디비가 &lt;b&gt;마지막 커넥션이 끊겼는데도 정상적으로 닫히지 않는다&lt;/b&gt;면 vm이 정상적으로 종료될 때, &lt;b&gt;shutdown hook&lt;/b&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;background-color: #fdfdfd; color: #000000;&quot;&gt;이 경우 vm 종료 시 데이터베이스가 계속 사용되므로(예: 종료 프로세스를 데이터베이스에 저장하기 위해) 이 경우 데이터베이스를 닫지 않아야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;이때 DB_CLOSE_ON_EXIT설정으로 &lt;b&gt;데이터베이스 자동 닫기를 사용 불가능&lt;/b&gt;으로 설정할 수 있다. &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;즉, 예상치 못한 디비의 셧다운을 막아주도록 한다.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;server mode&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;서버 모드도 &lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;내부적으로 서버 프로세스는 데이터베이스를 임베디드 모드로 열지만&lt;/span&gt;, 데이터의 처리 흐름이 TCP/IP를 통하여 전송되기 때문에 임베디드 모드보다는 느리다.&lt;/span&gt;&lt;/span&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;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;결론&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;h2는 용량이 작아서 상용보다는 주로 테스트에서 사용했다. 빠르게 테스트를 하고 결과를 구하기 위해서 서버 모드보다는 임베디드 모드가 더 좋은 것 같다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;또한 jdbc:h2:~/test 로 임베디드 모드로 url을 적는 경우는 test.mv.db 라는 file이 남아서 나중에 다른 애플리케이션에서 h2로 테스트를 할 때, 파일 이름이 겹쳐서 안되는 경우가 발생할 수 있으므로 매번 다른 이름으로 설정해야 한다는 번거러움이 있을 것 같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;또한 주로 테스트 용도로 h2를 사용하므로 공식문서에서도 말하듯 테스트를 할땐 인메모리 모드로 사용하는것을 추천한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;그리고 아직까지 mv.db파일을 활용할 일이 없어서 jdbc:h2:mem:test와 같은 url 이름으로 인메모리 디비를 사용하여 파일을 남지 않게 하는 것도 방법일것같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;또한 &lt;b&gt;;DB_CLOSE_DELAY=-1&lt;/b&gt; 은 &lt;b&gt;인메모리 디비에서만 사용된다.&lt;/b&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;b&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;;DB_CLOSE_ON_EXIT=FALSE&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;는 &lt;b&gt;임베디드 디비에서 사용&lt;/b&gt;하며 디비가 자동으로 종료되는 것을 막아준다. 이렇게 &lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;디비의 자동 종료를 비활성화하면 데이터베이스가 닫히는 시간을 스프링 부트가 제어할 수 있으므로 데이터베이스에 대한 액세스가 더 이상 필요하지 않을 때 발생하도록 보장한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbbC0g/btrGSyv6cQH/8QRLogXhDJ6lcouPmp7Y90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbbC0g/btrGSyv6cQH/8QRLogXhDJ6lcouPmp7Y90/img.png&quot; data-alt=&quot;@AutoConfigureTestDatabase&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbbC0g/btrGSyv6cQH/8QRLogXhDJ6lcouPmp7Y90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbbC0g%2FbtrGSyv6cQH%2F8QRLogXhDJ6lcouPmp7Y90%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;599&quot; height=&quot;207&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@AutoConfigureTestDatabase&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@AutoConfigureTestDatabase에서도 이렇게 설정된 이유가 위와 같은 이유때문일것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;Reference&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000;&quot;&gt;&lt;a href=&quot;http://www.h2database.com/html/features.html#in_memory_databases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://www.h2database.com/html/features.html#in_memory_databases&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/1.4.3.RELEASE/reference/html/boot-features-sql.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-boot/docs/1.4.3.RELEASE/reference/html/boot-features-sql.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;</description>
      <category>우아한테크코스 4기</category>
      <category>H2</category>
      <category>디비</category>
      <category>인메모리</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/126</guid>
      <comments>https://giron.tistory.com/126#entry126comment</comments>
      <pubDate>Sun, 10 Jul 2022 23:38:18 +0900</pubDate>
    </item>
    <item>
      <title>[Transactional]Spring 프레임워크는 트랜잭션을 어떻게 구현하였는가?</title>
      <link>https://giron.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;제목에 대한 질문에 답을 하지 못했다.이 질문에 대해서 정리를 해보고자 한다. 그전에 트랜잭션이 무엇인지 정리하고 시작하려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에 작용하는 여러 읽기와 쓰기를 수행하는 하나의 작업 단위&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 범위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 범위는 &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;826&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F6sr1/btrGbu0wNFI/auL5X9FFSFAiTu4jQPQGC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F6sr1/btrGbu0wNFI/auL5X9FFSFAiTu4jQPQGC1/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F6sr1/btrGbu0wNFI/auL5X9FFSFAiTu4jQPQGC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF6sr1%2FbtrGbu0wNFI%2FauL5X9FFSFAiTu4jQPQGC1%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;826&quot; height=&quot;668&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션이 다르다면 6에서 롤백이 일어나면, 3번과 5번에 해당한 쿼리&lt;b&gt;만&lt;/b&gt; 롤백이 된다. 즉, 4번에서 쿼리들은 롤백되지 않고 반영이 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 말로, 여러 메서드에서 하나의 트랜잭션을 갖고 싶다면 이 여러 메서드들을 하나의 커넥션을 사용하도록 하는 방법이 필요하다. -&amp;gt; 이러한 방법이 &lt;b&gt;트랜잭션 전파&lt;/b&gt;를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 전파&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 스프링에선 @Transactional을 통해 해당 어노테이션이 선언된 내부 메서드들도 한 커넥션에 묶인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLkr2i/btrGb8wcKkB/vkOLHocPIZp3nSng91jDCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLkr2i/btrGb8wcKkB/vkOLHocPIZp3nSng91jDCk/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLkr2i/btrGb8wcKkB/vkOLHocPIZp3nSng91jDCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLkr2i%2FbtrGb8wcKkB%2FvkOLHocPIZp3nSng91jDCk%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;1022&quot; height=&quot;570&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 범위 안에 외부 연동이 섞여 있으면 주의를 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 그림은 외부 api호출하다가 실패를 했을 때, 2번과 3번이 같이 롤백이 된다. 반면에 오른쪽을 보면 &lt;b&gt;2번과 외부 api가 성공을 했다&lt;/b&gt;. 그런데 4번에서 실패하면 &lt;b&gt;2번은 롤백이 되지만 외부API는 그대로 반영이 되었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;span style=&quot;color: #000000;&quot;&gt;, 롤백이 일어났다면 외부 시스템의 상태도 롤백하는 방법으로 진행해야 할 것같다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;외부 시스템 연동 후 어떤 문제가 발생하면 외부 시스템에 취소 요청을 보내는 방식을 보통 사용 합니다. 문제가 생긴 건을 모아서 일 배치로 하기도 하고, 문제 건을 주기적으로 확인해서 취소 요청을 하기도 합니다. 문제가 생겼을 때 즉시 취소 요청을 보내기도 하구요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://www.youtube.com/watch?v=urpF7jwVNWs&amp;amp;t=336s&quot;&gt;https://www.youtube.com/watch?v=urpF7jwVNWs&amp;amp;t=336s&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=urpF7jwVNWs&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/uDC1V/hyOVRGRYbP/kVzyF2toYcEmgA11u0Fnq0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/urpF7jwVNWs&quot; width=&quot;480&quot; height=&quot;360&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&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;REDO, UNDO&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 일관성을 지키기 위해서 DBMS는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로그 파일&lt;/b&gt;을 따로 보존하며 로그 파일에는 데이터베이스에서 일어난 모든 변화가 기록되어있다. 트랜잭션의 관리는 이 로그 파일의 기록을 바탕으로 이루어진다. 트랜잭션의 비정상적인 종료를 회복하기 위해서 DBMS는&lt;span&gt;&amp;nbsp;&lt;/span&gt;REDO&lt;span&gt;&amp;nbsp;&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;UNDO&lt;span&gt;&amp;nbsp;&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&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;해당 트랜잭션이 변경한 테이블은 트랜잭션 이전 상태로 복구&lt;/b&gt;되어야 한다. 이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;UNDO&lt;/b&gt;라고 한다. - rollback&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;REDO&lt;/b&gt;는 UNDO의 반대 개념으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이미 커밋한 트랜잭션의 수정을 재반영하는 연산&lt;/b&gt;이다. 만약 페이지 버퍼가 커밋 시점에 변경 사항을 모두 디스크에 반영한다면 REDO가 필요 없다. 하지만 거의 대부분의 DBMS는 효율성을 위해 &lt;b&gt;트랜잭션이 커밋되더라도 일정시간동안 이를 디스크에 반영하지 않고 버퍼에만 저장&lt;/b&gt;하고 있는데, 만약 이 시점에 문제가 생긴다면 REDO를 통해 수정 사항을 재반영 해주어야한다.&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;UNDO는 반영 중간에 잘못되면 RollBack 할 느낌, REDO는 중간 버퍼 느낌으로 반영했던 걸 커밋된 걸 다시 DB에 반영하는 느낌으로 가져가면 될 것 같다.&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;b&gt;UNDO&lt;/b&gt; 데이터도 모두 날아갑니다.&lt;/p&gt;
&lt;h3 id=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;장애로 인해 재시작되면 어떻게 복구가 되나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애 발생 이후 데이터베이스가 재시작 복구하는 경우에는 크게 3 단계로 복구가 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1 단계는&lt;/b&gt; 로그 분석 단계로, 마지막 체크포인트(checkpoint) 시점부터 최근 로그(EOL, End of Log)까지 로그를 탐색하면서 어디서부터 시스템이 복구를 시작해야 하는지, 어느 트랜잭션들을 복구해야 하는지 등등을 알아내는 단계이다.&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;/b&gt; REDO 복구 단계로 복구를 시작해야 하는 시점부터 장애 발생 직전 시점까지 REDO가 필요한 모든 로그를 REDO 복구를 하는 단계이다. 이 단계에서는 심지어 실패한 트랜잭션의 REDO 로그조차도 REDO를 하게 되는데, 언뜻 보면 불필요한 것으로 생각되지만 이렇게 하면 이후의 복구 단계를 매우 간단하게 하는 효과를 가져다 준다. 이 단계에서는 모든 트랜잭션에 대해서 REDO 복구만 한다는 점이 중요한데, 이러한 REDO 복구가 완료된 시점의 데이터베이스 상태는 장애 발생 시점의 상태와 같게 된다.&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 단계는&lt;/b&gt; UNDO 복구 단계로 로그를 최신 시점부터 다시 역방향으로 탐색하면서 UNDO 복구가 필요한 로그들에 대해서 UNDO 복구를 수행한다. 여기서 수행하는 UNDO는 결국 위에서 설명한 트랜잭션 철회 시에 수행하는 UNDO와 같은 방식으로, repeating history를 통해 데이터베이스 상태를 장애 시점까지 복원해두고 UNDO 복구를 여러 트랜잭션의 철회로 간단하게 해결할 수 있다. 한 트랜잭션만 철회시키는 것이 아니라 여러 트랜잭션을 철회시킨다는 차이점만 존재한다. 이 단계의 UNDO 복구를 개별 트랜잭션의 UNDO와 구별하여 Global UNDO라고도 부른다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;즉, REDO가&amp;nbsp;UNDO를 복구하고 최종적으로&amp;nbsp;UNDO가 복구(rollback)를 하게 됩니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 고립&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;READ UNCOMMITTED&lt;/b&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10번 트랜잭션이 데이터 수정하면 커밋 되기 전에 다른 트랜잭션이 조회하면 수정된걸로 조회(더티리드)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;READ COMMITTED(oracle)&lt;/b&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10번 트랜잭션이 데이터 수정하면 커밋이 되기 전에는 수정전 꺼, 된 후면 수정된 거로 조회&lt;/li&gt;
&lt;li&gt;ex)&lt;/li&gt;
&lt;li&gt;B 트랜잭션에서 10번 사원의 나이를 조회&lt;/li&gt;
&lt;li&gt;27살이 조회됨&lt;/li&gt;
&lt;li&gt;A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꾸고&amp;nbsp;커밋&lt;/li&gt;
&lt;li&gt;B 트랜잭션에서 10번 사원의 나이를 다시 조회&lt;/li&gt;
&lt;li&gt;28살이 조회됨&lt;/li&gt;
&lt;li&gt;문제는 한 트랜잭션이 두 번 조회 했는데 할때마다 결과가 달라짐(Non-Reapetable)(정합성 깨짐)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;REPETABLE READ(mysql)&lt;/b&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10번 트랜잭션이 500000번 사원을 조회&lt;/li&gt;
&lt;li&gt;12번 트랜잭션이 500000번 사원의 이름을 변경하고&amp;nbsp;커밋&lt;/li&gt;
&lt;li&gt;10번 트랜잭션이 500000번 사원을 다시 조회(변경전 이름 조회)&lt;/li&gt;
&lt;li&gt;(HOW? UNDO 영역에 백업된 데이터 반환)&lt;/li&gt;
&lt;li&gt;10번 트랜잭션을 닫고 다시 조회하면 변경된 이름 조회 성공&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;SERIALIZABLE&lt;/b&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InnoDB에서 기본적으로 순수한 SELECT 작업은 아무런 잠금을 걸지 않고 동작하는데,&lt;/li&gt;
&lt;li&gt;그냥 읽기만 할 때도 접근을 못하게 잠근다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Non-repeatableRead VS PhantomRead&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Non-repeatable reads are when your transaction reads committed&amp;nbsp;UPDATES&amp;nbsp;from another transaction. The same row now has different values than it did when your transaction began.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Phantom reads are similar but when reading from committed&amp;nbsp;INSERTS&amp;nbsp;and/or&amp;nbsp;DELETES&amp;nbsp;from another transaction. There are new rows or rows that have disappeared since you began the transaction.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 불가능한 읽기는 트랜잭션&amp;nbsp;이 다른 트랜잭션에서 커밋된&amp;nbsp;&lt;b&gt;업데이트를 읽을 때입니다.&lt;/b&gt;&amp;nbsp;이제 동일한 행에 트랜잭션이 시작되었을 때와 다른 값이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팬텀 읽기는 비슷하지만&amp;nbsp;다른 트랜잭션에서 커밋된 INSERT 및/또는 DELETES에서&amp;nbsp;&lt;b&gt;읽을&lt;/b&gt;&amp;nbsp;때&amp;nbsp;&lt;b&gt;입니다 .&lt;/b&gt;&amp;nbsp;거래를 시작한 이후 사라진 새로운 행이나 행이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RepeatableRead에서는 Non-repeatableRead를 undo영역에 저장되어 트랜잭션 id가 작은 것만 찾아서 조회하므로 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 insert했을 때는 update된게 아니므로 개수를 조회하면 +1되어서 찾아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT하는 레코드에 쓰기 잠금을 걸어야 하는데, 언두 영역에는 잠금을 걸 수 없기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;InnoDB에서 팬텀리드 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB 스토리지 엔진은 &lt;b&gt;레코드 락&lt;/b&gt;과 &lt;b&gt;갭 락&lt;/b&gt;을 합친 &lt;b&gt;넥스트 키 락&lt;/b&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;t&amp;nbsp;테이블에&amp;nbsp;c1 = 13&amp;nbsp;,&amp;nbsp;c = 17&amp;nbsp;인 두 레코드가 있다&lt;/b&gt;고 가정하자. 이때&amp;nbsp;&lt;b&gt;SELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE&amp;nbsp;&lt;/b&gt;쿼리를 수행하면,&amp;nbsp;&lt;b&gt;10 &amp;lt;= c1 &amp;lt;= 12,&lt;/b&gt;&amp;nbsp;&lt;b&gt;14 &amp;lt;= c1 &amp;lt;= 16&lt;/b&gt;,&amp;nbsp;&lt;b&gt;18 &amp;lt;= c1 &amp;lt;= 20&lt;/b&gt;&amp;nbsp;인 영역은 전부 갭 락에 의해 락이 걸려서 해당 영역에 레코드를 삽입할 수 없다. 또한 c = 13, c = 17인 영역도 레코드 락에 의해 해당 영역에 레코드를 삽입할 수 없다. 참고로 INSERT 외에 UPDATE, DELETE 쿼리도 마찬가지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식으로 InnoDB 스토리지 엔진은 넥스트 키 락을 이용하여 PHANTOM READ 문제를 해결한다.&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;● Record Lock : 각 인덱스 Record에 설정되는 Lock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;●&amp;nbsp;Gap Lock : 인덱스 Record 사이의 구간에 설정되는 Lock (Unique 인덱스에서 1건 데이터 변경시 Gap Lock 설정되지&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP를 통해서 활성화되고 트랜잭셔널의 advice가 메타데이터에 의해 구현이 되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/esid5f/btrGvTFvL3p/HzSVKqS5g4vlvilBSKR8SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/esid5f/btrGvTFvL3p/HzSVKqS5g4vlvilBSKR8SK/img.png&quot; data-alt=&quot;implementation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/esid5f/btrGvTFvL3p/HzSVKqS5g4vlvilBSKR8SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fesid5f%2FbtrGvTFvL3p%2FHzSVKqS5g4vlvilBSKR8SK%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;618&quot; height=&quot;382&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;implementation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 AOP를 통해 트랜잭션을 걸때 아래 문제를 주의하자!&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;private메서드는 AOP가 걸리지않는다. 왜냐하면 spring 2.5버전 이후부터는 default로 CGLIB을 사용하므로 상속을 통해 프록시를 구현한다. 하지만 private메서드는 상속이 불가능하기 때문에 AOP가 걸리지 않는다.&lt;/li&gt;
&lt;li&gt;동일한 클래스 내에서 @Transanctional이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;선언되지 않은 메소드&lt;/b&gt;에서 @Transactional이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;선언된 메소드를 호출&lt;/b&gt;해도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;트랜잭션이 적용되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;@Transactional(propagation=Propagation.REQUIRES_NEW)사용할 때,&lt;span&gt; &lt;/span&gt;동일한 클래스 메소드끼리 호출하면 새로 생성되지 않음. 반드시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;다른 클래스 메소드를 호출&lt;/b&gt;해야함.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://joont92.github.io/db/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-isolation-level/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://joont92.github.io/db/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-isolation-level/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <category>Transactional</category>
      <category>고립</category>
      <category>전파</category>
      <category>트랜잭션</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/125</guid>
      <comments>https://giron.tistory.com/125#entry125comment</comments>
      <pubDate>Sun, 10 Jul 2022 16:39:42 +0900</pubDate>
    </item>
    <item>
      <title>UX 워크샵</title>
      <link>https://giron.tistory.com/124</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2022.06.29 우아한테크코스에서 UX 워크샵을 진행했다. 평소 UX에 대한 관심은 있었지만 접해볼 경험이 없었는데 이러한 경험을 하게 되어서 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의형식이 아니라 직접 아이디어를 도출하고 구체화하고 평가하면서 어떻게 하면 user들의 입장에서 생각해 볼수있을지 경험해봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SDoaf/btrF59PM1OV/Z3sYjK2VFxXKBXZpK877p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SDoaf/btrF59PM1OV/Z3sYjK2VFxXKBXZpK877p0/img.png&quot; data-alt=&quot;아이디어 도출&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SDoaf/btrF59PM1OV/Z3sYjK2VFxXKBXZpK877p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSDoaf%2FbtrF59PM1OV%2FZ3sYjK2VFxXKBXZpK877p0%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;398&quot; height=&quot;1440&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아이디어 도출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pwKfa/btrF4hgzfwa/T7jlCpJpSTUPsvAsgB9MUK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pwKfa/btrF4hgzfwa/T7jlCpJpSTUPsvAsgB9MUK/img.jpg&quot; data-alt=&quot;how,now,might&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pwKfa/btrF4hgzfwa/T7jlCpJpSTUPsvAsgB9MUK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpwKfa%2FbtrF4hgzfwa%2FT7jlCpJpSTUPsvAsgB9MUK%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;420&quot; height=&quot;560&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;how,now,might&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 아이디어를 도출하고 어떤 아이디어가 좋은지?평가 하는 방식이라고 한다. wow영역으로 갈수록 좋은 아이디어라고 한다.&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;반면에 일본이나 중국은 튜토리얼을 진행하는 이용자가 많으므로 설명서등이 잘 명시되어져 있다고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 고객이 원하는 것에 초점을 맞추자&lt;/h3&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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초기 유저 고객은 최대 depth 3까지만 이용한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오톡이나 넷플릭스 또는 트위터를 보더라도 이용하는 페이지는 크게 3페이지를 넘어가지 않는다. 예를 들어 넷플릭스경우도 &lt;b&gt;1.메인 페이지에서 사용자 선택 -&amp;gt; 2.컨텐츠 선택 -&amp;gt; 3.시청&lt;/b&gt; 와 같은 순서로 진행이 되고 다른 유니콘 기업들도 마찬가지였다고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. [NUX 디자인] 유저가 원하는 것을 빠르게 보여주자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;nux&lt;/b&gt;(new user experience)란 &lt;span style=&quot;background-color: #ffffff; color: #292929;&quot;&gt;우리 상품을 처음 접하는 신규 사용자에게 우리 상품의 가치를 최대한 빠르고 강렬하게 전달하는 것을 목표로 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스가 성공한 이유를 봐보자. 은행 어플을 이용하는 대부분의 사람들은 계좌 이체가 목적이다. 따라서 토스는 메인화면에 &lt;b&gt;송금 버튼을 노출시킴으로써 사용자가 원하는 것을 빠르게 보여줬다.&amp;nbsp;&lt;/b&gt;이처럼 유저가 원하는 기능을 빠르게 보여주는 것도 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 초기 유니콘 기업들은 어떻게든 초기 유저를 모은다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트위터를 예시로 들자. 트위터는 팔로워가 7명 이상인 유저는 &lt;b&gt;진성 고객,&amp;nbsp;&lt;/b&gt;즉 트위터를 애용하는 고객이 된다는 결과를 얻었다. 그 후, 트위터는 메인 페이지에 유명한, 또는 지인들의 계정을 팔로우 할수있도록 노출시킴으로써 유저를 확보했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기</category>
      <category>nux</category>
      <category>UX</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/124</guid>
      <comments>https://giron.tistory.com/124#entry124comment</comments>
      <pubDate>Fri, 1 Jul 2022 20:44:16 +0900</pubDate>
    </item>
    <item>
      <title>레벨2를 하면서 생각해봤던 내용들</title>
      <link>https://giron.tistory.com/123</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. static vs bean&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;utils와 관련되면 static을 사용할 것이다. &lt;b&gt;어디서든 재사용 가능하고 도메인 로직에 포함이 되지 않는 기능, 상태를 갖지 않는 클래스 ex) 날짜를 스트링포멧으로 변경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 상태를 갖지않고, 외부 자원에 의존하지 않고 단순히 변환하는 일만을 담당하면 bean으로 등록할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성관계가 맺어지고 의존성이 필요하다면 bean&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;&lt;b&gt;static 함수 모음 클래스의 모든 함수는 인자가 동일할 경우 항상 동일한 결과를 리턴해야 한다. 이 규칙을 지킬 수 없으면 POJO Bean으로 만들라.&lt;br /&gt;이것이 이뤄지려면 함수 안에서는 외부 자원(Resource)에 대해 하나도 의존하면 안된다는 선결 조건을 충족해야 한다.&lt;/b&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;싱글톤과&amp;nbsp;정적&amp;nbsp;클래스&amp;nbsp;차이&lt;/b&gt; &lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;싱글톤은&amp;nbsp;&lt;b&gt;인터페이스를&amp;nbsp;만들수&amp;nbsp;있어서&amp;nbsp;다형성&amp;nbsp;활용가능&lt;/b&gt;하다.&lt;/span&gt;&lt;/span&gt;&lt;span&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&gt;출처: &lt;a href=&quot;http://kwon37xi.egloos.com/4844149&quot;&gt;http://kwon37xi.egloos.com/4844149 &lt;/a&gt;bclid=IwAR3bcsE7eAFI4xCk2kPai3tHU72JOauhS4kWYTEcwzyeHekUj_GNWQ1Cfao&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. service layer에서 dto반환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service layer에서 dto를 반환하면 controller에 도메인을 반환하지 않으므로 도메인에 대한 변경 가능성이 줄어들어서 더 안정적이다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. service layer에서 service참조 or repository 참조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;service 참조&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순환 참조가 발생할 가능성이 있다.&lt;/li&gt;
&lt;li&gt;A라는 서비스에서 B라는 서비스를 불러올 때, B의 레포지토리에서 B의 서비스를 만들어줘야하는데 이 때, A서비스를 위한 B서비스가 만들어지는 게 아닌가 라는 생각을 해볼 수도 있다.&lt;/li&gt;
&lt;li&gt;데이터가 분산되지 않고 관리하기 편할수있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Repository 참조&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;service -&amp;gt; repo 라는 단방향 계층으로 순환 참조같은 상황을 걱정하지 않아도 된다.&lt;/li&gt;
&lt;li&gt;여러 서비스에서 데이터를 쉽게 불러올수있어서 유연하다.&lt;/li&gt;
&lt;li&gt;데이터를 가져오는 로직이 여러 서비스에 분산되어서 관리하기 어려울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 스프링쓰는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바는 JVM이 안정적이다, 많은 회사에서 쓸만큼 대규모, 그리고&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;h3 data-ke-size=&quot;size23&quot;&gt;5. IOC 컨테이너에 SOLID원칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 ocp, dip와 관련있다. ocp는 확장에 열려있는 구조로써 ioc컨테이너를 사용하면 인터페이스를 사용하므로 다형성을 확보할수 있기 때문에 ocp와 관련이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dip는 의존 관계 역전 원칙으로 &lt;span style=&quot;background-color: #ffffff; color: #313131;&quot;&gt;클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. mock을 사용 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 명확한 단위 테스트가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ controller부분만 테스트하고 싶다면 mock을 사용하여 테스트가 가능하다.&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;Controller에서만 mock을 사용했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;controller에서만 mock을 사용했는데 이유는 restDocs를 사용하기 위해서 컨트롤러 테스트가 필요했고, restAsurred로 통합테스트 관점보다는 api문서화가 주 목적이므로 mock을 사용한 단위 테스트를 통해서 문서화를 구현했다. 그렇기 때문에 controller에서 mock을 사용했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄밀히 컨트롤러를 테스트하기 보다는 문서화를 위한 테스트였는데, 이것이 모든 controller에 대해서 테스트한다고 생각하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 컨트롤러가 단순히 외부 요청을 받고 서버에서 처리된 응답을 보내는 역할을 하므로 문서화를 사용한다면 기본적인(정상적인) 요청을 주고 받는 처리가 잘 작동한다고 확신할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.트랜잭션&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB&amp;nbsp;트랜잭션과&amp;nbsp;Applicaiton&amp;nbsp;트랜잭션중&amp;nbsp;DB트랜잭션에&amp;nbsp;의존되어&amp;nbsp;있어서 &lt;br /&gt;실패하면&amp;nbsp;application에&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;b&gt;트랜잭션과 Synchronized차이&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용되는 위치 차이: tansacional은 디비단에서 사용, file단위 로그를 남겨서 되돌린다.&lt;br /&gt;synchronized는 어플리케이션 단에서 사용하여 느림&lt;br /&gt;&lt;br /&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;</description>
      <category>우아한테크코스 4기</category>
      <category>mock</category>
      <category>service에서 dto반환</category>
      <category>static vs bean</category>
      <category>계층구조</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/123</guid>
      <comments>https://giron.tistory.com/123#entry123comment</comments>
      <pubDate>Mon, 27 Jun 2022 18:08:57 +0900</pubDate>
    </item>
    <item>
      <title>레벨2 - 장바구니</title>
      <link>https://giron.tistory.com/122</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. RestDocs vs swagger&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스웨거와는 다르게 프로덕션 코드에 어노테이션을 붙이지 않아서 좋다! 스웨거를 사용하면 뭔가 프로덕션 코드에서 테스트를 위한 코드가 짜여진 느낌?이 든다.&lt;/li&gt;
&lt;li&gt;레퍼런스가 많다! - 여러 레퍼런스들을 참고했었는데 자료들이 많아서 쉽게 적용할 수 있다!&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;운영 코드를 퓨어하게 가져가고 싶다면 레스트 독스를 사용하고 이를 포기하더라도 테스트 코드를 짜고 관리하는 비용을 지불할 경우에는 스웨거를 사용하는 느낌이죠.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 잦은 api요청의 영향&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;API 요청 하나도 내부 서버에서 무거운 로직을 실행하거나 DB를 오래 조회하는 쿼리를 날린다면 성능에 무리가 될 수 있어요. 또한 사용자도 기다리는 시간이 늘어나서 좋지 않은 경험을 하겠죠.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;물론 요청의 개수도 성능에 영향을 미쳐요. DB는 쓰레드 풀을 사용하여 커넥션을 맺을 수 있는 개수를 한정하는데 이를 넘어가게 되면 기다리게 되기 때문이에요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스 4기/코드리뷰</category>
      <category>마지막 미션</category>
      <category>우아한테크코스</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/122</guid>
      <comments>https://giron.tistory.com/122#entry122comment</comments>
      <pubDate>Mon, 27 Jun 2022 15:46:35 +0900</pubDate>
    </item>
    <item>
      <title>[인텔리제이] .http파일을 이용한 포스트맨 대체하기</title>
      <link>https://giron.tistory.com/121</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이에서 지원하는 .http라는 파일을 만들면 포스트맨을 대체하여 매우 편리했던 경험이 있었다. 이러한 경험을 글로 정리해서 공유해보려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;포스트맨을 사용했을 때는 매번 테스트 해볼때마다&amp;nbsp;복붙을 했었지만, .http라는 파일로 관리하여 저장할 수 있다.&lt;/li&gt;
&lt;li&gt;Code highlighting이 가능해서 더욱 직관적이다.&lt;/li&gt;
&lt;li&gt;코드 자동 완성의 기능들이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;음..굳이 뽑자면 파일이기 때문에 관리를 해야 한다? 인 것 같다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;POST 요청&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsb0LV/btrEVZoGCY7/lbdq68RYjj8wdXLfQ6sIgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsb0LV/btrEVZoGCY7/lbdq68RYjj8wdXLfQ6sIgk/img.png&quot; data-alt=&quot;회원가입&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsb0LV/btrEVZoGCY7/lbdq68RYjj8wdXLfQ6sIgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsb0LV%2FbtrEVZoGCY7%2Flbdq68RYjj8wdXLfQ6sIgk%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;518&quot; height=&quot;251&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원가입&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 POST로 요청을 보낼 때, Content-Type을 명시해주고 한 칸 띄우고 json형식으로 입력해주면, 테스트용 디비에 아래처럼 잘 들어간 것을 확인할 수 있다.&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;50&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bygwjW/btrEX7Z0888/kyMW4eQN0HTCagMrYQT2o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bygwjW/btrEX7Z0888/kyMW4eQN0HTCagMrYQT2o1/img.png&quot; data-alt=&quot;회원가입 완료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bygwjW/btrEX7Z0888/kyMW4eQN0HTCagMrYQT2o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbygwjW%2FbtrEX7Z0888%2FkyMW4eQN0HTCagMrYQT2o1%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;701&quot; height=&quot;50&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;50&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원가입 완료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 로그인에 성공을 하면, accessToken도 잘 나오는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kLs0f/btrEU2eU4mL/eUUsmMOZMddke0juuEND4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kLs0f/btrEU2eU4mL/eUUsmMOZMddke0juuEND4K/img.png&quot; data-alt=&quot;로그인 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kLs0f/btrEU2eU4mL/eUUsmMOZMddke0juuEND4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkLs0f%2FbtrEU2eU4mL%2FeUUsmMOZMddke0juuEND4K%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;1151&quot; height=&quot;211&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그인 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 만약 로그인 시에 예외가 발생하면 아래 사진처럼 예외까지 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcPm6r/btrEZbU9REL/HCQkFT5P2zAU7dk4nWhjLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcPm6r/btrEZbU9REL/HCQkFT5P2zAU7dk4nWhjLK/img.png&quot; data-alt=&quot;예외 발생시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcPm6r/btrEZbU9REL/HCQkFT5P2zAU7dk4nWhjLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcPm6r%2FbtrEZbU9REL%2FHCQkFT5P2zAU7dk4nWhjLK%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;410&quot; height=&quot;153&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예외 발생시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GET 요청&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxLejf/btrEZc0YMzX/0vD2lvsgj2e5kBu6881mPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxLejf/btrEZc0YMzX/0vD2lvsgj2e5kBu6881mPK/img.png&quot; data-alt=&quot;조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxLejf/btrEZc0YMzX/0vD2lvsgj2e5kBu6881mPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxLejf%2FbtrEZc0YMzX%2F0vD2lvsgj2e5kBu6881mPK%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;470&quot; height=&quot;132&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boFoDj/btrEWqsYfX1/XmDLpPY2kFPijwEBV7cK80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boFoDj/btrEWqsYfX1/XmDLpPY2kFPijwEBV7cK80/img.png&quot; data-alt=&quot;응답&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boFoDj/btrEWqsYfX1/XmDLpPY2kFPijwEBV7cK80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboFoDj%2FbtrEWqsYfX1%2FXmDLpPY2kFPijwEBV7cK80%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;1066&quot; height=&quot;338&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;응답&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GET요청은 단순히 url만 입력해주면 된다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰의 유효성 검사&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvoUeR/btrEXN17Ok3/YWNP8ZVakady8KjAEXPDk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvoUeR/btrEXN17Ok3/YWNP8ZVakady8KjAEXPDk0/img.png&quot; data-alt=&quot;장바구니 담기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvoUeR/btrEXN17Ok3/YWNP8ZVakady8KjAEXPDk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvoUeR%2FbtrEXN17Ok3%2FYWNP8ZVakady8KjAEXPDk0%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;959&quot; height=&quot;161&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;장바구니 담기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 장바구니 담기는 토큰을 헤더에 넣어주지 않아서 위와 같은 예외가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 해당 .http파일에서 헤더에 추가해도 좋지만 여러 api요청에 같은 일을 반복해야 하므로 따로 추출해주겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;활용&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AuthController.http&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9tq0Y/btrEZczRsre/LiMO0wuOW2t5WqsBtgkask/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9tq0Y/btrEZczRsre/LiMO0wuOW2t5WqsBtgkask/img.png&quot; data-alt=&quot;토큰 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9tq0Y/btrEZczRsre/LiMO0wuOW2t5WqsBtgkask/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9tq0Y%2FbtrEZczRsre%2FLiMO0wuOW2t5WqsBtgkask%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;831&quot; height=&quot;364&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;토큰 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 변수로 위와 같이 설정을 해준다. response.body에서 accessToken필드의 값을 주입해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;authorizationToken에 &quot;Bearer &quot; + 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;이후, 다시 토큰이 필요한 장바구니 담기 요청의 헤더에 추가를 해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3mQp8/btrEUO8kFv3/gGb2vK0CUngKpXwIoPX3K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3mQp8/btrEUO8kFv3/gGb2vK0CUngKpXwIoPX3K1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3mQp8/btrEUO8kFv3/gGb2vK0CUngKpXwIoPX3K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3mQp8%2FbtrEUO8kFv3%2FgGb2vK0CUngKpXwIoPX3K1%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;560&quot; height=&quot;223&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 로그인을 하고, 장바구니 담기 요청을 하면 정상 작동한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;http-client.env.json&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;{
  &quot;local&quot;: {
    &quot;apiUrl&quot;: &quot;http://localhost:8080/api/&quot;
  }
}&lt;/code&gt;&lt;/pre&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;477&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwl37L/btrEWHHR7uh/Ff6I41EK1BJ3wLOkpCIYSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwl37L/btrEWHHR7uh/Ff6I41EK1BJ3wLOkpCIYSK/img.png&quot; data-alt=&quot;{{apiUrl}}&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwl37L/btrEWHHR7uh/Ff6I41EK1BJ3wLOkpCIYSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwl37L%2FbtrEWHHR7uh%2FFf6I41EK1BJ3wLOkpCIYSK%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;477&quot; height=&quot;224&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;{{apiUrl}}&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;5-log&quot; data-ke-size=&quot;size23&quot;&gt;로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ의&lt;span&gt;&amp;nbsp;&lt;/span&gt;.http&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청은 모두 로그로 남기고 있다.&lt;br /&gt;Response Body가 너무 많을 경우엔 IntelliJ 화면에서 끊길 수가 있다고 합니다. 이럴 때 로그 파일로 확인하면 좋다.&lt;br /&gt;로그 파일의 위치는 {프로젝트폴더}/.idea/httpRequests/&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;301&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TPRbI/btrEYz275S4/K5jix2kXmkqeYV2bMi2St1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TPRbI/btrEYz275S4/K5jix2kXmkqeYV2bMi2St1/img.png&quot; data-alt=&quot;log&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TPRbI/btrEYz275S4/K5jix2kXmkqeYV2bMi2St1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTPRbI%2FbtrEYz275S4%2FK5jix2kXmkqeYV2bMi2St1%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;301&quot; height=&quot;188&quot; data-origin-width=&quot;301&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;log&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;http-request-log.http&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/266&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jojoldu.tistory.com/266&lt;/a&gt;&lt;/p&gt;</description>
      <category>백앤드 개발일지/웹, 백앤드</category>
      <category>.http</category>
      <category>인텔리제이</category>
      <category>포스트맨</category>
      <author>giron</author>
      <guid isPermaLink="true">https://giron.tistory.com/121</guid>
      <comments>https://giron.tistory.com/121#entry121comment</comments>
      <pubDate>Wed, 22 Jun 2022 17:20:29 +0900</pubDate>
    </item>
  </channel>
</rss>