<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>JiwonDev</title>
    <link>https://jiwondev.tistory.com/</link>
    <description>밤새는걸 좋아하는 예비 개발자</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 02:08:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JiwonDev</managingEditor>
    <image>
      <title>JiwonDev</title>
      <url>https://tistory1.daumcdn.net/tistory/4410826/attach/e9710f49a4e446d3bb140b77424acf5f</url>
      <link>https://jiwondev.tistory.com</link>
    </image>
    <item>
      <title>PostgreSQL 와 vaccum에 대해 알아보자.</title>
      <link>https://jiwondev.tistory.com/297</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ReV23/btsDLJFkekt/sOWA0ErDsplrq9EcF3mUN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ReV23/btsDLJFkekt/sOWA0ErDsplrq9EcF3mUN1/img.png&quot; data-alt=&quot;PostgreSQL 뿌-우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ReV23/btsDLJFkekt/sOWA0ErDsplrq9EcF3mUN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FReV23%2FbtsDLJFkekt%2FsOWA0ErDsplrq9EcF3mUN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;150&quot; height=&quot;155&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PostgreSQL 뿌-우&lt;/figcaption&gt;
&lt;/figure&gt;
&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;  PostgreSql&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 사용자들이 가장 쉽게 적응할 수 있는 오픈소스 DBMS이다. postgres(포스트그리)라고도 많이 부른다. 조회&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;성능적인 측면에서 뛰어나기 때문에 MySQL처럼 함께 많이 사용된다. &lt;a href=&quot;http://www.postgresql.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;http://www.postgresql.org/&lt;/a&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;1738&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vM0Id/btsDIvVZKA3/E4YNxmkxgKVYASmLE9IRKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vM0Id/btsDIvVZKA3/E4YNxmkxgKVYASmLE9IRKk/img.png&quot; data-alt=&quot;https://d2.naver.com/helloworld/227936 , 1986년 Ingres에서 시작하여 1995년에 Postgres95 이름이 처음 붙여졌다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vM0Id/btsDIvVZKA3/E4YNxmkxgKVYASmLE9IRKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvM0Id%2FbtsDIvVZKA3%2FE4YNxmkxgKVYASmLE9IRKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;195&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://d2.naver.com/helloworld/227936 , 1986년 Ingres에서 시작하여 1995년에 Postgres95 이름이 처음 붙여졌다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/6550/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/6550/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1705913827645&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;Aurora MySQL vs Aurora PostgreSQL | 우아한형제들 기술블로그&quot; data-og-description=&quot;안녕하세요, 클라우드스토리지개발팀 정지원 입니다. 최근 저희 팀에서는 Aurora MySQL로 운영되고 있던 대량 통계성 DB를 Aurora PostgreSQL로 이관하는 것을 검토중입니다. 그래서 오늘은 준비 과정에&quot; data-og-host=&quot;techblog.woowahan.com&quot; data-og-source-url=&quot;https://techblog.woowahan.com/6550/&quot; data-og-url=&quot;https://techblog.woowahan.com/6550/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Qh186/hyU8UQIt1w/1ACGSKycWic4QfJotTSypK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856,https://scrap.kakaocdn.net/dn/JIC4z/hyU84sg2YN/vVPm3oSup8t9l9PoA5Yfk0/img.png?width=600&amp;amp;height=372&amp;amp;face=0_0_600_372&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/6550/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://techblog.woowahan.com/6550/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Qh186/hyU8UQIt1w/1ACGSKycWic4QfJotTSypK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856,https://scrap.kakaocdn.net/dn/JIC4z/hyU84sg2YN/vVPm3oSup8t9l9PoA5Yfk0/img.png?width=600&amp;amp;height=372&amp;amp;face=0_0_600_372');&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;Aurora MySQL vs Aurora PostgreSQL | 우아한형제들 기술블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 클라우드스토리지개발팀 정지원 입니다. 최근 저희 팀에서는 Aurora MySQL로 운영되고 있던 대량 통계성 DB를 Aurora PostgreSQL로 이관하는 것을 검토중입니다. 그래서 오늘은 준비 과정에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;techblog.woowahan.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; &lt;span&gt;&amp;nbsp; PostgreSQL 는 어떤 기준으로 사용해야할까요?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;중요한 내용은 아니라서, 궁금하면 눌러보자&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전세계에서 가장 많이 사용하는 데이터베이스를 한번 훑어서 비교해보면 아래와 같다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1439&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lKDRv/btsDGMqgeqN/o8WnbgMYw2nR8GI01LeYxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lKDRv/btsDGMqgeqN/o8WnbgMYw2nR8GI01LeYxk/img.png&quot; data-alt=&quot;https://db-engines.com/en/ranking - 2021년 10월 기준&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lKDRv/btsDGMqgeqN/o8WnbgMYw2nR8GI01LeYxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlKDRv%2FbtsDGMqgeqN%2Fo8WnbgMYw2nR8GI01LeYxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1439&quot; height=&quot;488&quot; data-origin-width=&quot;1439&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://db-engines.com/en/ranking - 2021년 10월 기준&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Orcale과 Microsoft 는 유료 데이터베이스 서비스이고 MySQL과 PostgreSQL은 오픈소스 무료 데이터베이스이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 MySQL과 PostgreSQL의 인기가 많으며 회사나 관공서처럼 사후지원이 필요한 상용서비스는 비싼 요금을 지불하고 Oracle, Microsoft를 주로 사용한다고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 2010년대는 다양한 DBMS들이 우후죽순으로 탄생하던 시기였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모바일의 등장으로 전통적인 관계형 데이터베이스, RDBMS의 시대는 저문다는 이야기가 나왔고 NoSQL과 같은 단순한 로그 성격의 빅데이터의 저장이 필요해지며 Cassandra나 HBase, 나중에 나온 MongoDB등이 많은 관심을 받았었다.&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;하지만 10년이 지난 지금, 그 누구도 그때 출시됐던 NoSQL DBMS가 상용 데이터베이스를 잠식했다고는 언급하지 않는 듯 하다.&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: #0593d3;&quot;&gt;* 캐시 서버로 많이 사용되는 Redis와 Memcached는 서비스 DBMS라고 보기에는 범주가 약간 다르다. ElasticSearch도 마찬가지&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그나마 HBase와 MongoDB정도가 자기만의 영역에서 사용되고 있으며 예상과는 다르게 여전히 MySQL, Postgres와 같은 관계형 DB가 수많은 NoSQL DBMS의 역할을 대체하며 사용되고 있다.&lt;/p&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;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;오픈소스 DB 대결, MySQL vs Postgres&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 무료 DBMS중에선 MySQL 사용률이 압도적이었으나 2009년에 오라클이 Sun사를 인수하면서 MySQL의 소유권 이슈, 유료화등의 문제 때문에 PostgreSQL 사용률이 증가했고 이후 이래의 장점들 덕분에 사용률이 꾸준하게 증가되고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #777777; text-align: center;&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=8018&amp;amp;utm_source=slack&amp;amp;utm_medium=bot&amp;amp;utm_campaign=T03385HMGMD&quot;&gt;https://news.hada.io/topic?id=8018&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=11287&amp;amp;utm_source=slack&amp;amp;utm_medium=bot&amp;amp;utm_campaign=T017EBSUEL8&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://news.hada.io/topic?id=11287&lt;/a&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wvPEc/btsDHrGhaXm/5SxGwXNJ0j8E1Xr9AHfpW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wvPEc/btsDHrGhaXm/5SxGwXNJ0j8E1Xr9AHfpW0/img.png&quot; data-alt=&quot;https://news.hada.io/topic?id=8018&amp;amp;amp;utm_source=slack&amp;amp;amp;utm_medium=bot&amp;amp;amp;utm_campaign=T03385HMGMD&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wvPEc/btsDHrGhaXm/5SxGwXNJ0j8E1Xr9AHfpW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwvPEc%2FbtsDHrGhaXm%2F5SxGwXNJ0j8E1Xr9AHfpW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;378&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://news.hada.io/topic?id=8018&amp;amp;utm_source=slack&amp;amp;utm_medium=bot&amp;amp;utm_campaign=T03385HMGMD&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PostgreSQL은&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://jiwondev.tistory.com/75?category=872861&quot;&gt;SQL 표준&lt;/a&gt;을 경쟁사보다 잘 지키고 있는 편이다.&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://velog.io/@jisoo1170/Oracle-MySQL-PostgreSQL-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80#postgresql&quot;&gt;다양한 join 방법&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(sort merge join , hash join)을 제공하여 복잡한 조회 쿼리 성능이 좋다.&lt;/li&gt;
&lt;li&gt;PostGIS(좌표 계)와 같은 다양한 확장 플러그인을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;update 를 과거 행을 삭제하고 새로운 행을 추가하는 형식이라 경쟁사에 비해 성능이 떨어지는 편이다&lt;br /&gt;즉  수정보다 insert 를 주로 하고 복잡한 조회가 많은 서비스에서 적합하다.&lt;/li&gt;
&lt;li&gt;MVCC  가 추가 저장하는 식으로 구현되어있다. 이후 마치 Java GC 처럼 auto vaccum을 통해 죽은 데이터들을 정리한다. 즉 auto vaccum 동작에 대해 잘 알고 있지않고 사용한다면 죽은 데이터가 쌓여 성능 저하를 일으킬 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  Postgre  MVCC 와 vaccum&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MVCC(Multi-Version Concurrency Control)는 여러 트랜잭션에서 락을 걸지 않고 동시성을 관리해 성능을 높이는 방법을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;postgreSQL의 MVCC는 수정 후 데이터를  통으로 insert한 뒤 나중에 따로 vaccum 해준다. 이 방법으로 동시성과 성능을 극대화시켰다. &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;3802&quot; data-origin-height=&quot;1670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2dOnE/btsDGphBKx3/YYt6rypzW0uJBDthuEc6jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2dOnE/btsDGphBKx3/YYt6rypzW0uJBDthuEc6jK/img.png&quot; data-alt=&quot;MySql은 undo log로 구현한거라 일정시간 이후 메모리에서  정리한다. postgre는 같은 공간에 데이터를 계속 쌓는 형식이라 GC(vaccum)이 필요하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2dOnE/btsDGphBKx3/YYt6rypzW0uJBDthuEc6jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2dOnE%2FbtsDGphBKx3%2FYYt6rypzW0uJBDthuEc6jK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3802&quot; height=&quot;1670&quot; data-origin-width=&quot;3802&quot; data-origin-height=&quot;1670&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySql은 undo log로 구현한거라 일정시간 이후 메모리에서  정리한다. postgre는 같은 공간에 데이터를 계속 쌓는 형식이라 GC(vaccum)이 필요하다.&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: #0593d3;&quot;&gt;(* 트랜잭션ID = tx_id , 데이터 레코드 = Tuple )&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;Postgre MVCC는 트랜잭션이 시작하게 되면 본인의 id 를 할당받는다.&lt;/li&gt;
&lt;li&gt;이후 UPDATE등을 할 때 page (=Free Space Map page)를 생성하고 변경될 때 마다 현재의 스냅샷을 insert 형식으로 쌓아간다.&lt;/li&gt;
&lt;li&gt;MVCC 구현을 위해 현재 데이터 페이지 내에 자신의 tx_id 보다 작은값의 id를 가진 Tuple을 읽어온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 각 Tuple에 할당되는 tmin, tmax값은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;xmin &lt;span style=&quot;color: #0593d3;&quot;&gt;(*tx_id min)&lt;/span&gt;&lt;br /&gt;- INSERT&lt;span&gt;&amp;nbsp;&lt;/span&gt;시&amp;nbsp; -&amp;gt; 신규 Tuple 의 xmin 에 tx_id 할당&lt;br /&gt;- UPDATE&lt;span&gt;&amp;nbsp;&lt;/span&gt;시 -&amp;gt; 신규 Tuple 의 xmin 에 tx_id 할당&lt;/li&gt;
&lt;li&gt;xmax &lt;span style=&quot;color: #0593d3;&quot;&gt;(*tx_id max)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;신규 Tuple 의 xmax 에는 NULL 할당&lt;/span&gt;&lt;br /&gt;DELETE&lt;span&gt;&amp;nbsp;&lt;/span&gt;시 -&amp;gt; 이전 Tuple 의 xmax 에 tx_id 할당&lt;br /&gt;UPDATE&lt;span&gt;&amp;nbsp;&lt;/span&gt;시 -&amp;gt; 이전 Tuple 의 xmax 에 tx_id 할당,&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정 속에서 여러번 변경되어 쓸모없어진 이전 데이터들은 어디에도 참조되지 않는데, 이를 dead tuple이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 dead tuple은 수동 명령어로 지울 수도 있지만 다행히도 postgre auto vaccum을 통해 대부분 처리된다.&lt;/p&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;   vaccum이 대체 뭐죠? 안하면 어떻게 되는거죠?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Au2hr/btsDGK0eYIR/I1xQ27VHtVLlNEvGba2mWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Au2hr/btsDGK0eYIR/I1xQ27VHtVLlNEvGba2mWk/img.png&quot; data-alt=&quot;참고로 서비스 트래픽이 엄청난 대기업이라면, Postgre auto-vaccum 설정도 반드시 확인해보아야한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Au2hr/btsDGK0eYIR/I1xQ27VHtVLlNEvGba2mWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAu2hr%2FbtsDGK0eYIR%2FI1xQ27VHtVLlNEvGba2mWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1812&quot; height=&quot;386&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참고로 서비스 트래픽이 엄청난 대기업이라면, Postgre auto-vaccum 설정도 반드시 확인해보아야한다.&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;우선 Postgre에서는 Vaccum과 Vaccum full 2가지 종류가 있는데, full 의 경우 디스크 메모리까지 정리하지만 테이블에서 베타락을 걸어 DML, 쿼리 어떠한 것도 하지 못하고 시간도 오래걸린다. 운영도중에는 가능하면 vaccum full 작업을 피해야 하기에 auto vaccum 기본 값은 vaccum full 작업을 하지 않게 설정되어있다. &lt;span style=&quot;color: #0593d3;&quot;&gt;디스크 메모리 부족 등 최악의 경우에만 사용하자.  단순히 특정 테이블에서 생긴 문제라면 ALTER TABLE로 새로운 테이블을 만드는게 더 낫다. + 참고로 Truncate로 통째로 날리면 dead tuple도 함께 정리된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1705843000922&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- DB 전체 풀 실행
vacuum full analyze;

-- DB 전체 간단하게 실행
vacuum verbose analyze;

-- 해당 테이블만 간단하게 실행
vacuum analyse [테이블 명];

-- 특정 테이블만 풀 실행
-- 참고로 full 실행시 pg_class.relfilenode 가 변경되는데, 이는 디스크에 저장되는 물리적인 파일명이다.
vacuum full [테이블명];&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;일반 vaccum 작업은 락은 안걸리지만 Disk I/O 부하를 만들고 동시에 작업중인 다른 DB 세션의 성능을 떨어뜨린다. 그래서 수동으로 실행시 사용량이 적은 새벽시간대에 vaccum 명령을 하는 것을 권장한다. 물론 이는 &lt;a href=&quot;https://postgresql.kr/docs/13/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-VACUUM-COST&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;관련 설정 변경&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;다행히 이는 사용자가 매번 신경 쓸 필요가 없도록 auto-vaccum에 의해 처리된다.&lt;br /&gt;auto-vaccum은 메모리 공간을 줄인다기보단, 주기적으로 vaccum을 돌려 일정 수준 이상 커지지 않게 만들어주도록 동작한다. 또한 죽은 데이터를 삭제한 후 쿼리(인덱스) 통계 정보가 바뀔 필요가 있다고 판단되면 ANALYZE 명령을 추가로 수행한다. &lt;span style=&quot;color: #0593d3;&quot;&gt;단순히 삭제된 데이터수를 기준으로 판단하는건 아니고, 칼럼의 변경빈도나 자료형등 여러 조건을 고려하여 현재 정리한 테이블에만 analyze를 한다.&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;600&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ANRMy/btsDNI7cxZE/8ZtHMSh9YhfwWY8FLM9mVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ANRMy/btsDNI7cxZE/8ZtHMSh9YhfwWY8FLM9mVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ANRMy/btsDNI7cxZE/8ZtHMSh9YhfwWY8FLM9mVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FANRMy%2FbtsDNI7cxZE%2F8ZtHMSh9YhfwWY8FLM9mVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;337&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;455&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auto-vaccum 은  여러개의 데몬 프로세스(autovaccum launcher)로 항상 실행되어 있으며 테이블의 자료가 많이 변경되었을 때&amp;nbsp; (autovaccum worker) 프로세스를 만들어서 자동으로 실행된다.  이는 다른 작업을 방해하지 않으나  share update lock의 경우에만 vaccum이 중단되고,  해당 락을 계속 잡고 있는 경우 vaccum이 제대로 완료되지 않을 수 있다. &lt;span style=&quot;color: #0593d3;&quot;&gt;당연한말이지만 db max_connection 같은 옵션과 autovaccum 프로세스는 아무런 관련이 없다.&lt;/span&gt;&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;참고로 현재 실행중인 vaccum 세션은 아래와 같이 확인해볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705843790648&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT datname, usename, pid, CURRENT_TIMESTAMP - xact_start AS xact_runtime, query
FROM pg_stat_activity
WHERE upper(query) LIKE '%VACUUM%'
ORDER BY xact_start;&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;1400&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf7x27/btsDKesidhQ/XT3BkqbwH3dRmLRAXREJR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf7x27/btsDKesidhQ/XT3BkqbwH3dRmLRAXREJR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf7x27/btsDKesidhQ/XT3BkqbwH3dRmLRAXREJR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf7x27%2FbtsDKesidhQ%2FXT3BkqbwH3dRmLRAXREJR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;214&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;PostgreSQL&lt;/span&gt;에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://postgresql.kr/docs/13/sql-vacuum.html&quot;&gt;&lt;span&gt;VACUUM&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령은 다음과 같은 여러 가지 이유로 정기적으로 각 테이블 단위로 실행되어야 한다&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: left;&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt; 죽은 tuple  때문에 생기는 불필요한 추가 Disk I/O와 메모리 공간을 줄이기 위해&amp;nbsp; 필요하다.&lt;/li&gt;
&lt;li&gt;트랜잭션 ID 겹침이나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;다중 트랜잭션 ID 겹침&lt;span&gt;&amp;nbsp;&lt;/span&gt;상황으로 오래된 자료가 손실 될 가능성을 방지해야할 필요가 있다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;PostgreSQL&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿼리 실행 계획기가 사용할 자료 통계 정보(ANALYZE)를 갱신할 필요가 있다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt; &lt;/span&gt;커버리지&amp;nbsp;인덱스(only&amp;nbsp;index)&amp;nbsp;속도를&amp;nbsp;위해,&amp;nbsp;실자료&amp;nbsp;지도(visibility&amp;nbsp;map,&amp;nbsp;vm)&amp;nbsp;갱신할&amp;nbsp;필요&amp;nbsp;있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 죽은 tuple 때문에 생기는 불필요한 추가 Disk I/O와 메모리 공간을 줄이기 위해 필요하다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postgre는 데이터를 읽을 때 FSM page (Free Space Map page) 단위로 읽어들인다. 이는 기본 8KB인데  페이지에 쓰레기 데이터가 많다면 더 많은 FSM page를 메모리에 올려야한다. 즉 메모리가 낭비되는건 물론이고 Disk I/O 작업 횟수를 늘리게 되고  쌓이면 쌓일수록 쿼리의 성능 저하로 이어지게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvhxXT/btsDKai4iNd/zOhyGkDPfTRXVoy3zIhjy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvhxXT/btsDKai4iNd/zOhyGkDPfTRXVoy3zIhjy0/img.png&quot; data-alt=&quot;원래 2페이지로 끝날 걸 죽은 데이터 때문에 수십만 개의 페이지를 더 읽어야할지 모른다. 이는 곧 Disk I/O의 증가 = 쿼리의 성능 하락을 의미한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvhxXT/btsDKai4iNd/zOhyGkDPfTRXVoy3zIhjy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvhxXT%2FbtsDKai4iNd%2FzOhyGkDPfTRXVoy3zIhjy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;977&quot; height=&quot;335&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;335&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;원래 2페이지로 끝날 걸 죽은 데이터 때문에 수십만 개의 페이지를 더 읽어야할지 모른다. 이는 곧 Disk I/O의 증가 = 쿼리의 성능 하락을 의미한다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ 트랜잭션 ID 겹침으로 오래된 자료가 손실 될 가능성을 방지해야할 필요가 있다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postgre MVCC에 사용되는 트랜잭션 ID는 32bit 양수이기에 42억 개까지만 저장할 수  있다. 이 수를 초과하면 오버플로우로 0이 되면서 데이터가 손실될 가능성이 있다. 문제를 방지하기위해 설정된 수치를 넘게되면 auto-vaccum 을 이용해 테이블의 모든 트랜잭션 ID를  따로 예약된 Fronzen xid(2)로 바꿔버린다. 이를 freeze (또는 anti wraparound vaccum)라고 부른다. &lt;span style=&quot;color: #0593d3;&quot;&gt;참고로 Postgre는 트랜잭션 ID가 Fronzen xid로 되어있다면, 값을 비교하지않고 무조건 옛날 Tuple로 판단한다.&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;600&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zq398/btsDGXyMUyE/gzaWuxFoJfUqSTuFZKY1Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zq398/btsDGXyMUyE/gzaWuxFoJfUqSTuFZKY1Nk/img.png&quot; data-alt=&quot;최대값이 40억개라, 20억개가 되기전에 Freeze 해서 초기화해야 겹침없이 XID를 쌓아서 쓸 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zq398/btsDGXyMUyE/gzaWuxFoJfUqSTuFZKY1Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZq398%2FbtsDGXyMUyE%2FgzaWuxFoJfUqSTuFZKY1Nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;324&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최대값이 40억개라, 20억개가 되기전에 Freeze 해서 초기화해야 겹침없이 XID를 쌓아서 쓸 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 PostgreSQL 9.4 이전에는 단순히 xmin = 2 (Fronze xid) 로 덮어씌워서 저장했는데 새 버전에서는 별도의 flag(1bit) 값으로 표시해두고 두고 xmin을 변경하지는 않기에 장애 분석용으로 사용될 수도 있다.&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;span style=&quot;color: #24292e; text-align: start;&quot;&gt;vacuum_freeze_min_age 으로 변경할 수 있으며, 기본 최대값은 5000만이다.&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; 실제 동작에서는 즉시 auto- vaccum이 실행되는 건 아니고 freeze가 필요한 Tuple이 생기면 테이블 파라메타(&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;vacuum_freeze_table_age&lt;/span&gt;)&amp;nbsp; 값에 저장해두었다가 설정된 임계치(&lt;span style=&quot;color: #0593d3;&quot;&gt;autovacuum_freeze_max_age&lt;/span&gt;) 를 넘게되면 강제로 freeze(Anti Wraparound Vaccum) 를 수행한다.&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;그럴 일은 없겠지만 auto-vaccum이 무한정 실패해서 트랜잭션ID 겹침문제가 발생할 우려가 있다면, 아래의 경고 메시지를 띄워주고&lt;/p&gt;
&lt;pre id=&quot;code_1705837799489&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WARNING:  database &quot;mydb&quot; must be vacuumed within 10985967 transactions
HINT:  To avoid a database shutdown, execute a database-wide VACUUM in that database.&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;이를 무시하고 계속 사용한다면 아래 오류 메시지를 남기며 Postgre 서버는 모든 트랜잭션 작업을 중단한다.&lt;/p&gt;
&lt;pre id=&quot;code_1705837807094&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ERROR:  database is not accepting commands to avoid wraparound data loss in database &quot;mydb&quot;
HINT:  Stop the postmaster and use a standalone backend to VACUUM in &quot;mydb&quot;.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 우리 DB에서 오래된 트랜잭션ID가 어느정도인지 궁금하다면, pg_class 와 pg_database 테이블로 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705837949848&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- relfrozenxid 은 vaccum 정리했던 기준 트랜잭션ID값 이다. (이 값보다 오래된 모든ID는 freeze 처리됨)
-- age는 현재 시점기준, vaccum으로 정리된 가장 오래된 트랜잭션ID값 이다.
SELECT c.oid::regclass as table_name,
       greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age
FROM pg_class c
LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
WHERE c.relkind IN ('r', 'm');

SELECT datname, age(datfrozenxid) FROM pg_database;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 Postgre 9.3 이후로 &lt;a href=&quot;https://postgresql.kr/docs/13/routine-vacuuming.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다중 트랜잭션 ID 겹침문제가 공식문서&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;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ &lt;span&gt;PostgreSQL&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿼리 실행 계획기가 사용할 자료 통계 정보(ANALYZE)를 갱신할 필요가 있다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 위에도 언급했지만, auto-vaccum 과정속에서 필요에 따라 ANALYZE를 추가로 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 DB 버전을 바꾸거나 마이그레이션하는 경우 실행계획이 크게 달라져 장애가 발생할 수 있는데, 이때 쿼리를 계속 실행시켜주거나 ㅇANALYZE 명령을 입력하면 Postgre 서버가 알맞은 실행계획으로 최적화 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 anaylze는 아래와 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705838830778&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 데이터베이스
analyze;

# 테이블
analyze t1;

# 컬럼
analyze t1 (c1, c2);

# 상세 수행내역 확인
analyze verbose t1;
-- INFO:  analyzing &quot;public.t1&quot;
-- INFO:  &quot;t1&quot;: scanned 443 of 443 pages, containing 100000 live rows and 0 dead rows; 30000 rows in sample, 100000 estimated total rows
-- ANALYZE&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;또한 아래의 설정을 건드린 적이 없다면, 기본적으로 auto-vaccum 은 실행하면서 통계 정보를 같이 수집하여 판단 기준에 사용하는데 필요하면  pg_class, pg_stats 등의 테이블로 확인해볼 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;autovacuum (기본값 on) : autovacuum 프로세스 사용 여부&lt;/li&gt;
&lt;li&gt;track_cou nts (기본값 true): 데이터베이스 통계정보 수집여부&lt;/li&gt;
&lt;li&gt;autovacuum_analyze_scale_factor (기본값0.1) : 테이블 내의 레코드 변경 비율&lt;/li&gt;
&lt;li&gt;autovacuum_analyze_threshold (기본값 50) : 최소 변경 레코드 수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1705843110550&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#------------------------------------------------------------------------------
# postgresql.conf 파일 내 AUTOVACUUM PARAMETERS
#------------------------------------------------------------------------------
#autovacuum = on                        # Enable autovacuum subprocess?  'on'  ## PostgreSQL 9.x 부터는 default로 활성화된 상태임
                                        # requires track_counts to also be on.
#log_autovacuum_min_duration = -1       # -1 disables, 0 logs all actions and   ## 0면  autovacuum 일어날때마다 로그 남기고,
                                        # their durations, &amp;gt; 0 logs only           200ms 설정하면 200ms 이상 걸려 실행되는
                                        # actions running at least this number
                                        # of milliseconds.
#autovacuum_max_workers = 3             # max number of autovacuum subprocesses
                                        # (change requires restart)
#autovacuum_naptime = 1min              # time between autovacuum runs     ## 실행 주기.. 1min 넘 짧네..
#autovacuum_vacuum_threshold = 50       # min number of row updates before ## vacuum 작업시 최소 갯수.. 1000이상으로 늘려도 됨
                                        # vacuum
#autovacuum_analyze_threshold = 50      # min number of row updates before ## 마찬가지 500이상
                                        # analyze 
#autovacuum_vacuum_scale_factor = 0.2   # fraction of table size before vacuum
#autovacuum_analyze_scale_factor = 0.1  # fraction of table size before analyze
#autovacuum_freeze_max_age = 200000000  # maximum XID age before forced vacuum
                                        # (change requires restart)
#autovacuum_vacuum_cost_delay = 20ms    # default vacuum cost delay for
                                        # autovacuum, in milliseconds;
                                        # -1 means use vacuum_cost_delay
#autovacuum_vacuum_cost_limit = -1      # default vacuum cost limit for
                                        # autovacuum, -1 means use
                                        # vacuum_cost_limit&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;1694&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rYBID/btsDLscvVvB/rkht0qfPDywojjrgijM8s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rYBID/btsDLscvVvB/rkht0qfPDywojjrgijM8s0/img.png&quot; data-alt=&quot;실제로 쿼리 실행계획 (EXPLAIN ANALYZE)을 측정할 때 해당 통계 테이블들을 사용한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rYBID/btsDLscvVvB/rkht0qfPDywojjrgijM8s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrYBID%2FbtsDLscvVvB%2Frkht0qfPDywojjrgijM8s0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;333&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제로 쿼리 실행계획 (EXPLAIN ANALYZE)을 측정할 때 해당 통계 테이블들을 사용한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://seunghyunson.tistory.com/20&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;nbsp;Postgre 실행계획 및 통계 - 쿼리성능 분석하기 seunghyunson&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;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣&amp;nbsp;  커버리지 인덱스(only index) 속도를 위해, vm(&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;visibility map, vm)&lt;/span&gt; 갱신할 필요 있다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 ANAYLZE와 는 조금 다른내용인데, postgre는 vaccum 작업을 통해 내부의 visibility map을 갱신해서 현재와 미래의 모든 트랜잭션이 실제로 사용할 테이블별 지도를 미리 만들어둔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JLJrK/btsDJkNjvUt/TIky69wDDKBbU8MmKdtYC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JLJrK/btsDJkNjvUt/TIky69wDDKBbU8MmKdtYC1/img.png&quot; data-alt=&quot;https://postgresql.kr/docs/13/storage-vm.html 를 구글번역한 내용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JLJrK/btsDJkNjvUt/TIky69wDDKBbU8MmKdtYC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJLJrK%2FbtsDJkNjvUt%2FTIky69wDDKBbU8MmKdtYC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2150&quot; height=&quot;614&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://postgresql.kr/docs/13/storage-vm.html 를 구글번역한 내용&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;이는 인덱스 전용 쿼리들 (커버리지 인덱스) 에 대해 빠른 응답을 제공하는데 사용된다.  PostgreSQL의 인덱스는 어떤 자료를 어떤 DB 세션에 보여줘야할지 결정하기 위해 테이블의 page를 읽어보는데,&amp;nbsp; 만약 vaccum 때 미리 만들어둔 visibility map 만으로 찾아서 실제 page 데이터를 검색하지 않고  바로 찾기에 데이터가 많은 테이블을 조회할 때 Disk I/O를 크게 줄일 수 있다.&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  실제로 있었던 Vaccum 이슈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내에서 사용자의 학습상태를 관리하기위해 SpringStateMachine를 사용해서 각각의 사용자의 학습상태(Context) 객체를 직렬화한 뒤 계속 업데이트하여 DB에 저장하고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_d.png&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UOZU5/btsDJBBhzRn/7n1kteHuUF1WjzKAmVtjf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UOZU5/btsDJBBhzRn/7n1kteHuUF1WjzKAmVtjf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UOZU5/btsDJBBhzRn/7n1kteHuUF1WjzKAmVtjf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUOZU5%2FbtsDJBBhzRn%2F7n1kteHuUF1WjzKAmVtjf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;107&quot; data-filename=&quot;edited_d.png&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1705840728832&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table state_machine (
     machine_id varchar(255) not null,
     state varchar(255),
     state_machine_context oid,
     primary key (machine_id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgres에서는 lobs(Large Objects) 를 저장할 때 식별용 oid 값 칼럼에 저장해두고 실제  데이터는 pg_largeobject 테이블에 따로 저장된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xZ8H6/btsDJZPpFV6/D7IxdQTI3VNRYKmW8BdSj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xZ8H6/btsDJZPpFV6/D7IxdQTI3VNRYKmW8BdSj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xZ8H6/btsDJZPpFV6/D7IxdQTI3VNRYKmW8BdSj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxZ8H6%2FbtsDJZPpFV6%2FD7IxdQTI3VNRYKmW8BdSj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;58&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  그래서 뭐가 문제였죠?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제가 되었던건 oid 값은 auto-vaccum이 처리해주지만 large_object에 저장되어있는 데이터는 oid와의 링크 때문에 dead tuple로 취급되지 않아 지워지지 않았다&lt;span style=&quot;color: #000000;&quot;&gt;. 아마 oid를 업데이트하더라도  기존 데이터 수정이 아닌 새로운 row가 추가되는 형식이었기 때문으로 추측한다.&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;학습상태 특성상 굉장히 자주 업데이트 되는데,  위 테이블에 보이는 data(Hex)가 수정 될 때 마다 dead tuple이 계속 쌓이게 되어 수동으로 vaccum을 해주지 않으면 점점 성능이 느려지는 이슈가 있었다. &lt;span style=&quot;color: #0593d3;&quot;&gt;  위에도 언급했지만 Dead Tuple이 쌓이면 불필요한 page를 엄청나게 읽어 Disk I/O가 증가하는건 물론 실행계획이 바뀌어 인덱스 자체를 안타게 변경될 수도 있다.&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;문제해결은 간단했다. 아래 명령을 통해 oid 와 링크를 해제해주면 auto-vaccum이 동작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1705841303365&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT lo_unlink($oid값)&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;a href=&quot;https://github.com/postgres/postgres/blob/master/contrib/vacuumlo/vacuumlo.c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Postgre github에 이미 만들어둔 VaccumIO&lt;/a&gt;가 이미 있다. 이는 모든 테이블에 oid와 pg_largeobject의 oid를 비교하여 지워진 oid를 unlink 하도록 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 주의해야할 것은 lo_unlink 자체 의 쿼리가 무겁고, lock을 걸기에 운영중에는 조심해서 사용해야한다. 테스트해보니 아예 서비스를 중지해야할 정도는 아니라서 새벽 시간대 배치 스크립트를 통해 VaccumIO를 실행하도록 해서 해결했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;아래 두 스크립트를 도커 이미지에 말아서 `docker run` 으로 실행할 수 있게 만들어 새벽마다 vaccumIo를 실행하도록 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1705841763024&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# vacuumioscript.sh

#!/bin/bash
echo &quot;*:*:*:*:$DB_PW&quot; &amp;gt; ~/.pgpass
chmod 600 ~/.pgpass
vacuumlo -h ${DB_URL} -p 5432 -U postgres -w ${DRY_RUN} -v lms -l ${LIMIT}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1705841827498&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM postgres:14.3-alpine

ENV DB_URL=...
ENV DB_PW=...
ENV DRY_RUN=&quot;--dry-run&quot; # vaccumlo --dry-run 을통해 현재 DB가 large object를 사용하는지 확인가능
ENV LIMIT=500

COPY vacuumioscript.sh vacuumioscript.sh
RUN chmod 775 vacuumioscript.sh

ENTRYPOINT [&quot;./vacuumioscript.sh&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 당시에는  별 생각없이 큰데이터? 그럼 LOB에 저장하면 되겠지하고 선택했고 vaccum 동작에 대해서도 무지했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 더 찾아보니 LargeObject(oid)가 아닌 bytea 로 바로 저장하면 auto-vaccum이 정상적으로 처리됨을 확인하고 타입을 변경해서 해결했다. &lt;span style=&quot;color: #0593d3;&quot;&gt;물론 일반적인 테이블에 lob -&amp;gt; bytea 로 변경하면서 조회 성능이 저하될 수 있지만, 해당 테이블은  stateContext 의 크기가 고정되어있기도 하고 사용자 1명이 고유한 ID로 자신의 데이터만 사용하는 구조라 별 문제 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1705842391600&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;jiwon_state_machine&quot;)
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator::class)
class LmsStateMachineEntity : RepositoryStateMachine() {

  @Id
  @Column(name = &quot;machine_id&quot;)
  private var machineId: String? = null

  @Column(name = &quot;state&quot;)
  private var state: String? = null

  // @Lob 제거, bytea 타입으로 바로 저장
  @Column(name = &quot;state_machine_context&quot;, length = 10240)
  private var stateMachineContext: ByteArray? = null
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1705842279573&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table jiwon_state_machine (
     machine_id varchar(255) not null,
     state varchar(255),
     state_machine_context bytea,
     primary key (machine_id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  다른 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 Postgre를 딥하게 사용한다면 한번씩 겪는 문제가 Vaccum 이슈인데, 내가 겪은 @Lob과 같은 특수한 상황이 아니더라도 데이터량이 많아지면 auto-vaccum이 잘 되지않아 Dead Tuple로 인해 비슷한 이슈가 발생할 수 있다. &lt;a href=&quot;https://medium.com/29cm/postgresql-autovacuum-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91%EA%B8%B0-1-8284955c0193&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;아래 29CM Auto-vaccum 장애 대응기&lt;/a&gt;도 꼭 한번 읽어보기를 추천한다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1705843914569&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;PostgreSQL Autovacuum 장애 대응기 (1)&quot; data-og-description=&quot;29CM에서는 Amazon RDS for PostgreSQL를 사용하고 있습니다. 최근에 경험한 PostgreSQL Autovacuum 장애와 Vaccum 최적화 방법에 대해서 설명하고자 합니다.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/29cm/postgresql-autovacuum-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91%EA%B8%B0-1-8284955c0193&quot; data-og-url=&quot;https://medium.com/29cm/postgresql-autovacuum-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91%EA%B8%B0-1-8284955c0193&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LviO0/hyU8Yk1XYE/xTHxwTNQnuAa66in69B70K/img.jpg?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://medium.com/29cm/postgresql-autovacuum-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91%EA%B8%B0-1-8284955c0193&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/29cm/postgresql-autovacuum-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91%EA%B8%B0-1-8284955c0193&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LviO0/hyU8Yk1XYE/xTHxwTNQnuAa66in69B70K/img.jpg?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&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;PostgreSQL Autovacuum 장애 대응기 (1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;29CM에서는 Amazon RDS for PostgreSQL를 사용하고 있습니다. 최근에 경험한 PostgreSQL Autovacuum 장애와 Vaccum 최적화 방법에 대해서 설명하고자 합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&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;h2 data-ke-size=&quot;size26&quot;&gt;  레퍼런스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/227936&quot;&gt;한눈에 살펴보는 PostgreSQL - https://d2.naver.com/helloworld/227936&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;a href=&quot;https://techblog.woowahan.com/9478/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;PostgreSQL Vaccum에 대한 거의 모든 것 https://techblog.woowahan.com/9478/&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;a href=&quot;https://repost.aws/ko/articles/ARbMXOvsOvTeCuiuoxSZxuwA/amazon-aurora-postgre-sql-auto-vacuum-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1-2&quot;&gt;AWS Article 1편- https://repost.aws/ko/articles/ARbMXOvsOvTeCuiuoxSZxuwA/amazon-aurora-postgre-sql-auto-vacuum-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1-2&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;a href=&quot;https://repost.aws/ko/articles/ARdulMK-V2QJueMZ5SmKnk8Q/amazon-aurora-postgre-sql-auto-vacuum-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-2-2&quot;&gt;AWS Artice 2편 - https://repost.aws/ko/articles/ARdulMK-V2QJueMZ5SmKnk8Q/amazon-aurora-postgre-sql-auto-vacuum-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-2-2&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;a href=&quot;https://postgresql.kr/docs/13/routine-vacuuming.html&quot;&gt;Postgre13 Docs - https://postgresql.kr/docs/13/routine-vacuuming.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;&lt;a href=&quot;https://blog.gaerae.com/2015/09/postgresql-vacuum-fsm.html&quot;&gt;Vaccum을 실행해야하는 이유, 성능향상 - https://blog.gaerae.com/2015/09/postgresql-vacuum-fsm.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;&lt;a href=&quot;https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;Postgre Wiki - 개발철학 https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server&lt;/a&gt;&lt;/p&gt;</description>
      <category>  2025/DB(MySQL,PostgreSQL)</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/297</guid>
      <comments>https://jiwondev.tistory.com/297#entry297comment</comments>
      <pubDate>Sun, 26 May 2024 13:04:56 +0900</pubDate>
    </item>
    <item>
      <title>Cache#1 스프링의 캐시 추상화(@Cacheable)</title>
      <link>https://jiwondev.tistory.com/282</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/08rWR/btrKJ6gJT9H/OexB6JlmrYBLJYcepmUv3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/08rWR/btrKJ6gJT9H/OexB6JlmrYBLJYcepmUv3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/08rWR/btrKJ6gJT9H/OexB6JlmrYBLJYcepmUv3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F08rWR%2FbtrKJ6gJT9H%2FOexB6JlmrYBLJYcepmUv3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2696&quot; height=&quot;1046&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크는 다른 서비스와의 통합 &amp;amp; 기능 추상화를 제공해준다. (RedisTemplate,&amp;nbsp;RestTemplate,&amp;nbsp;MailSender....)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spring.io/blog/2011/02/11/spring-framework-3-1-m1-released/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2012년 스프링 3.1&lt;/a&gt;버전부터 캐시 추상화(Cache Abstraction)을 제공해주며, 이는 &lt;a href=&quot;https://spring.io/blog/2011/02/11/spring-framework-3-1-m1-released/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2014년 스프링 4.1&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/current/reference/html/integration.html&quot;&gt;스프링 프레임워크의 다른 서비스와의 통합(Integration) 관련 문서&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1661622357385&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;Integration&quot; data-og-description=&quot;As a lightweight container, Spring is often considered an EJB replacement. We do believe that for many, if not most, applications and use cases, Spring, as a container, combined with its rich supporting functionality in the area of transactions, ORM and JD&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.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;Integration&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;As a lightweight container, Spring is often considered an EJB replacement. We do believe that for many, if not most, applications and use cases, Spring, as a container, combined with its rich supporting functionality in the area of transactions, ORM and JD&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  캐시에 대한 이해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/283&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.08.28 - [  Spring Framework] - Cache#2 캐시에 대한 이해&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1661636195922&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;Cache#2 캐시에 대한 이해&quot; data-og-description=&quot;  Cache  Cache는 컴퓨터 과학에서, 데이나 값을 미리 복사해놓는 임시 저장소를 가르킨다.&amp;nbsp;한글로는 고속완충기억기..지만 아무도 그렇게 안부른다 성능향상을 위해 결과를 캐싱(저장)하여 값을&quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/283&quot; data-og-url=&quot;https://jiwondev.tistory.com/283&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jAA7U/hyPBSJ8f58/pPZn2LzQrdG4gxL1U5hG01/img.png?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/mnIqs/hyPBPNpgm8/wKKKfNPOYTmDwQJcxlVmn0/img.png?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/cGjzIy/hyPAtd8MeC/5OuOgtV93bkKpLNX4YerH1/img.png?width=3632&amp;amp;height=1018&amp;amp;face=0_0_3632_1018&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/283&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/283&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jAA7U/hyPBSJ8f58/pPZn2LzQrdG4gxL1U5hG01/img.png?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/mnIqs/hyPBPNpgm8/wKKKfNPOYTmDwQJcxlVmn0/img.png?width=800&amp;amp;height=457&amp;amp;face=0_0_800_457,https://scrap.kakaocdn.net/dn/cGjzIy/hyPAtd8MeC/5OuOgtV93bkKpLNX4YerH1/img.png?width=3632&amp;amp;height=1018&amp;amp;face=0_0_3632_1018');&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;Cache#2 캐시에 대한 이해&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Cache  Cache는 컴퓨터 과학에서, 데이나 값을 미리 복사해놓는 임시 저장소를 가르킨다.&amp;nbsp;한글로는 고속완충기억기..지만 아무도 그렇게 안부른다 성능향상을 위해 결과를 캐싱(저장)하여 값을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  스프링의 캐시 추상화 (&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Cache Abstraction&lt;/a&gt;)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어노테이션 기반으로 캐시를 추상화하여 사용할 수 있다. 이는 Spring Data 의 @Transcational 과 굉장히 유사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP 기반으로 메서드의 결과를 캐싱하여 사용할 수 있다. (* 캐싱된 경우, 메서드를 실행하지 않고 바로 결과를 반환한다.)&lt;/p&gt;
&lt;pre id=&quot;code_1661622790477&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Cacheable(&quot;bestSeller&quot;) // cacheName: bestSeller, cachekey : BookNo로, 메서드 결과를 캐싱한다.
public Book getBestSeller(String bookNo) {
   return result
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 캐시 추상화에 사용되는 객체들은 org.springframework.cache 패키지에서 찾아볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1661622656242&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.cache.Cache
org.springframework.cache.CacheManager&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;1280&quot; data-origin-height=&quot;781&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chKyLT/btrKHcPOXYH/5npU0pspiATkRzE7akC2n1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chKyLT/btrKHcPOXYH/5npU0pspiATkRzE7akC2n1/img.png&quot; data-alt=&quot;스프링 4.1부터는 자바 표준인 JSR-107 어노테이션도 지원합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chKyLT/btrKHcPOXYH/5npU0pspiATkRzE7akC2n1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchKyLT%2FbtrKHcPOXYH%2F5npU0pspiATkRzE7akC2n1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;781&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;781&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스프링 4.1부터는 자바 표준인 JSR-107 어노테이션도 지원합니다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  스프링 프레임워크에 캐시 구현체(저장소)는 없다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache, CacheManager는 캐시 사용을 추상화하는거지, 실제 캐시 저장소는 포함하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시에 대한 세부설정(TTL, 정책)이나 멀티 스레드 동기화등은 캐시 저장소 구현체가 처리해야한다. 이는 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-specific-config&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;@Cacheable 같은 어노테이션들은 스프링 빈으로 등록되어있는 CacheManager 인터페이스를 이용하여 캐시를 적용시킨다.&lt;br /&gt;이를 코드로 직접 구현해도 되지만, 스프링부트에서 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/io.html#io.caching.provider&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;아래와 같은 캐시 구현체들을 CacheManager로 쉽게 만들 수 있게 제공&lt;/a&gt;해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1661623211042&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스프링부트를 이용하여 현업에서 자주 사용하는 캐시 구현체들은 대부분 쉽게 사용할 수 있다.

implementation 'org.springframework.boot:spring-boot-starter-cache'// 스프링 application 내부 캐시
implementation 'org.springframework.boot:spring-boot-starter-data-redis'// redis
implementation 'com.github.ben-manes.caffeine:caffeine'// caffenie&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;1610&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwtWXi/btrKGw843pN/Hhobpr9NlnWFiY6Gc8XrM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwtWXi/btrKGw843pN/Hhobpr9NlnWFiY6Gc8XrM1/img.png&quot; data-alt=&quot;해당 캐시구현체는 스프링 부트에서 설정 연동이나 아예 spring-boot-starter-...를 추가적으로 제공한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwtWXi/btrKGw843pN/Hhobpr9NlnWFiY6Gc8XrM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwtWXi%2FbtrKGw843pN%2FHhobpr9NlnWFiY6Gc8XrM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;572&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;해당 캐시구현체는 스프링 부트에서 설정 연동이나 아예 spring-boot-starter-...를 추가적으로 제공한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 캐시 구현체들은 따로 설정해주지 않아도, 설정파일(yml) 기반으로 캐시 설정이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clG2lw/btrKHVGKnQM/fFxhBTCUGSMwFUn2ZhL1kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clG2lw/btrKHVGKnQM/fFxhBTCUGSMwFUn2ZhL1kK/img.png&quot; data-alt=&quot; 자세한건 공식문서를 참고하자.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clG2lw/btrKHVGKnQM/fFxhBTCUGSMwFUn2ZhL1kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclG2lw%2FbtrKHVGKnQM%2FfFxhBTCUGSMwFUn2ZhL1kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;324&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;780&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  스프링의 캐시 추상화 적용방법 (@EnableCaching)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하다. @Configuration 클래스에 &lt;b&gt;@EnableCaching&lt;/b&gt; 를 추가하고, CacheManager 인터페이스를 구현하는 빈을 등록하면 된다.&lt;br /&gt;한번 더 언급하는데, 스프링의 캐시 추상화 어노테이션들은 CacheManager 를 통해 캐시를 사용한다.&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;CacheManager 의 설정 방법은 캐시 구현체마다 다를 수 있다. 이는 글 하단에 다시 설명하니, 일단은 가볍게 보고 넘어가자&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661623583896&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Configuration
  @EnableCaching
  class RedisCacheConfig {
    
    // 스프링에서 캐시 추상화는 CacheManager 객체를 통해 작동한다. 해당 빈을 등록해주자
    @Bean
    fun userCacheManager(objectMapper: ObjectMapper, redisProperties: RedisProperties): CacheManager {
      
      // RedisClient 구현체를 Lettuce 로 변경 (스프링-부트-레디스의 기본 구현체 : Jedis )
      val redisConnectionFactory = LettuceConnectionFactory(redisProperties.host, redisProperties.port)

      // Redis 캐시설정
      val redisConfiguration = RedisCacheConfiguration.defaultCacheConfig()
        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer ()))
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer ()))
        .disableCachingNullValues()
        .entryTtl(Duration.ofHours(5L));

      // 스프링에서 사용할 CacheManager 빈 생성 (spring-boot-starter-data-redis)
      return RedisCacheManager.RedisCacheManagerBuilder
        .fromConnectionFactory(redisConnectionFactory)
        .cacheDefaults(redisConfiguration)
        .build()
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 CacheManager 인터페이스를 직접 구현해도 되지만, 스프링에서는 이미 아래와 같이 다앙한 구현체를 제공하고 있다.&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: #ef5369;&quot;&gt;ConcurrentMapCacheManager&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 간단하게 ConcurrentHashMap을 사용하는 CocurrentMapCache 를 사용한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;SimpleCacheManager&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 프로퍼티를 이용해서 사용할 캐시를 직접 등록해주는 방법(&lt;span&gt;기본적으로 제공하는 캐시가 없다)&lt;br /&gt;&lt;/span&gt;&amp;nbsp;이는 스프링 Cache 인터페이스를 구현해서 캐시 클래스를 직접 만드는 경우 테스트에서 사용하기에 적당하다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;CompositeCacheManager&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 하나 이상의 캐시 매니저를 사용하도록 지원해주는 혼합 캐시 매니저다.&lt;br /&gt;여러 캐시 저장소를 동시에 사용해야 할 때 쓴다. cacheManagers 프로퍼티에 적용할 캐시 매니저 빈을 모두 등록해주면 된다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;NoOpCacheManager&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;:&lt;span&gt;&lt;span&gt;&amp;nbsp;&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;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;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;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;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;span&gt;.&lt;/span&gt;&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;JCacheCacheManager&lt;/span&gt;: 자바 표준 JSR107 기반으로 동작하는 구현체를 위한 캐시매니저&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;RedisCacheManager&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Redis 지원하는 캐시매니저&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;EhCacheCacheManager&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: EhCache 지원하는 캐시매니저&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;CaffeineCacheManager&lt;/span&gt;: Caffeine 지원하는 캐시매니저&lt;/li&gt;
&lt;li&gt;... 생략&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  캐시 사용방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 캐시는 기본적으로 &lt;b&gt;메서드 단위의 AOP&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;span style=&quot;color: #0593d3;&quot;&gt;  @Transcational과 동일하게, @Cacheable이 걸려있는 &lt;a href=&quot;https://ch4njun.tistory.com/265&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;클래스 내부 메서드를 호출(Self-invocation)&lt;/a&gt;하면 AOP가 동작하지 않는다.&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;878&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ag0mI/btrKGOBKjQa/TBYLJFc24qswPwF8dAkvAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ag0mI/btrKGOBKjQa/TBYLJFc24qswPwF8dAkvAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ag0mI/btrKGOBKjQa/TBYLJFc24qswPwF8dAkvAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAg0mI%2FbtrKGOBKjQa%2FTBYLJFc24qswPwF8dAkvAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;878&quot; height=&quot;258&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SPEL 문법 ( &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions-language-ref&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt; )&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 따라 캐시를 다르게 동작하게 하거나, 캐시 key를 지정하고 싶다면 SPEL 문법을 사용하여 설정할 수 있다.&lt;br /&gt; 스프링 캐시는 기본적으로 &lt;b&gt;메서드의 파라메타&lt;/b&gt;를 캐시 key 로 사용한다. &lt;span style=&quot;color: #ef5369;&quot;&gt;( #값, #객체 명.변수명, #객체명.메서드()) &lt;span style=&quot;color: #000000;&quot;&gt;로 사용하면 된다.&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661624368656&quot; class=&quot;less&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 특정 파라미터를 키로 사용
@Cacheable(value=&quot;product&quot;, key=&quot;#productNo&quot;)
Product bestProduct(String productNo, User user, Date dateTime) {
	
}

// 파라미터의 특정 프로퍼티 값을 키로 사용
@Cacheable(value=&quot;product&quot;, key=&quot;#condition.productNo&quot;)
Product bestProduct(SearchCondition condition) {
	
}

// 사용자의 타입이 관리자인 경우에만 캐시를 적용 (user.type 프로퍼티가 &quot;ADMIN&quot;인 경우에만 캐시 적용)
@Cacheable(value=&quot;user&quot;, condition=&quot;#user.type == 'ADMIN'&quot;)
public User findUser(User user) {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt; &lt;/span&gt;&lt;br /&gt;만약 반환값이나 메서드의 이름등 다른 값을 사용하고 싶다면 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-spel-context&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Cache전용 SPEL 문법&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;1730&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rzV0b/btrKHaK9oW7/7MkD3kLfTfNket7p6ahQkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rzV0b/btrKHaK9oW7/7MkD3kLfTfNket7p6ahQkk/img.png&quot; data-alt=&quot;예를 들어 (메서드 반환 객체의 id 필드)를 캐시 키로 사용하고싶다면, key=&amp;quot;#result.id&amp;quot; 로 작성하면 된다. 자세한건 공식문서를 참고하자&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rzV0b/btrKHaK9oW7/7MkD3kLfTfNket7p6ahQkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrzV0b%2FbtrKHaK9oW7%2F7MkD3kLfTfNket7p6ahQkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1730&quot; height=&quot;686&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예를 들어 (메서드 반환 객체의 id 필드)를 캐시 키로 사용하고싶다면, key=&quot;#result.id&quot; 로 작성하면 된다. 자세한건 공식문서를 참고하자&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;h3 data-ke-size=&quot;size23&quot;&gt;@Cacheable&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(조건에 따라) 메서드 결과값을 캐시로 저장한다.&lt;br /&gt;여러 개의 캐시 이름을 붙일 수 있으며, 최소 하나 이상의 cache hit이 존재한다면, 메서드를 실행하지 않고 해당 캐시 값을 바로 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1661626626393&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(cacheNames=&quot;books&quot;, key=&quot;#isbn&quot;)
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames=&quot;books&quot;, key=&quot;#isbn.rawNumber&quot;)
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661624113984&quot; class=&quot;reasonml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// unless = &quot;..SPEL..&quot; 의 결과가 true이면 캐싱하지 않는다.
@Cacheable(value=&quot;spittleCache&quot; unless=&quot;#result.message.contains('NoCache')&quot;)
Spittle findOne(long id);

// condition = &quot;..&quot; 결과가 true 이면 캐싱한다.
// 물론 unless 와 condition을 둘 다 적어도 된다.
@Cacheable(value=&quot;spittleCache&quot; unless=&quot;#result.message.contains('NoCache')&quot; condition=&quot;#id &amp;gt;= 10&quot;)
Spittle findOne(long id);

// 참고로 코틀린처럼 ?. null-safety 문법도 제공해준다 ㅋ 
@Cacheable(cacheNames=&quot;book&quot;, condition=&quot;#name.length() &amp;lt; 32&quot;, unless=&quot;#result?.hardback&quot;)
public Optional&amp;lt;Book&amp;gt; findBook(String name)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Cacheable(value=&quot;..&quot;) 는 @Cacheable(cacheName=&quot;..&quot;) 과 같은 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 탈 조건은 condition, 캐시를 타지 않을 조건은 unless 에 적으면 된다. 필요하다면 둘 다 적어도 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dD2EGy/btrKK9qUFCk/TgDorLG2xtFDwQGKXnz4a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dD2EGy/btrKK9qUFCk/TgDorLG2xtFDwQGKXnz4a1/img.png&quot; data-alt=&quot;cacheable 설정 값&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dD2EGy/btrKK9qUFCk/TgDorLG2xtFDwQGKXnz4a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdD2EGy%2FbtrKK9qUFCk%2FTgDorLG2xtFDwQGKXnz4a1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1840&quot; height=&quot;1226&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;cacheable 설정 값&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;  주의 - condition은 메서드를 호출하기 전에, unless는 메서드를 호출한 뒤에 평가한다. (&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-annotations-cacheable-condition&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Cacheable(condition=&quot;..&quot;)은 메서드를 호출하기 전에 SPEL을 실행해서 true인지 확인한다.&lt;br /&gt;즉 &lt;b&gt;메서드의 파라메타가 아닌, 메서드의 반환값 (#result) 등의 조건은 사용할 수 없다&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;반환값을 캐시 키로 사용하고 싶은 경우, unless 조건만을 사용해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꿀팁을 하나 주자면 CacheableResult 같은 전용 객체를 하나 만들어서 사용하면 cache hit 여부도 알 수 있고, SPEL 조건도 깔끔해진다.&lt;/p&gt;
&lt;pre id=&quot;code_1661624756024&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 내가 만든 Cache용 객체
class CacheableResult&amp;lt;T&amp;gt;(
  val data: T,
  val isFailed: Boolean = false // 기본 값을 isFailed=false 로 설정
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661624868021&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* 메서드 실행 후를 평가하고 싶다면 unless 를 사용해야한다. 이 때 이런식으로 구성하면 코드 가독성이 좋아진다. */
@Cacheable(value = [&quot;myCacheName&quot;], unless = &quot;#result.isFailed()&quot;)
fun getData(categoryId: String): CacheableResult&amp;lt;List&amp;lt;CategoryRes&amp;gt;&amp;gt; {

    return CacheableResult(
    	data = data,
        isFailed = true // 이렇게 캐시 여부를 명시적으로 지정할 수 있다.
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@CacheEvict,&amp;nbsp; @CachePut&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@CacheEvict&amp;nbsp; : (조건에 따라) 캐시를 삭제한다. 참고로 Evict은 쫓아내다, 퇴거하다는 의미의 단어이다.&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;1658&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Ivfe/btrKGpoF3gm/sMbjYWWgBivLOqeRtwUZk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Ivfe/btrKGpoF3gm/sMbjYWWgBivLOqeRtwUZk0/img.png&quot; data-alt=&quot;beforeInvocation, allEntries는 삭제(CacheEvict)에만 존재하는 설정이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Ivfe/btrKGpoF3gm/sMbjYWWgBivLOqeRtwUZk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Ivfe%2FbtrKGpoF3gm%2FsMbjYWWgBivLOqeRtwUZk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;970&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;970&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;beforeInvocation, allEntries는 삭제(CacheEvict)에만 존재하는 설정이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1661665068663&quot; class=&quot;less&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// bestProduct 캐시의 내용을 제거한다
@CacheEvict(value=&quot;bestProduct&quot;)
public void refreshBestProducts() {
	// ...
}

// productNo와 같은 키값을 가진 캐시를 제거한다.
@CacheEvict(value=&quot;product&quot;, key=&quot;#product.productNo&quot;)
public void updateProduct(Product product) {
	// ...
}

// 그냥 통째로 삭제한다.
@CacheEvict(value = &quot;bestSeller&quot;, allEntires = true)
public void clearBestSeller() {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@CachePut : 메서드 실행을 방해하지 않고, 캐시를 업데이트한다. (메서드는 언제나 실행된다.)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 특정 캐시를 삭제하고 싶다면 @CacheEvict 을 사용하자.
@CacheEvict(value = [&quot;product&quot;], key = &quot;#id&quot;)
fun updateProduct(long id, dto: UpdateProductDto) {
  val product: Product = repository.findById(id)

  product.update(
    name = dto.name,
    price = dto.price,
    owner = dto.owner
  )
}

// 메서드 결과가 캐시가 되었음에도 메서드를 항상 실행시키고 싶다면, @CachePut을 사용하자
@CachePut(cacheNames = [&quot;exampleStore&quot;], key = &quot;#cacheData.value&quot;, condition = &quot;#cacheData.value.length() &amp;gt; 5&quot;)
fun updateCacheData(cacheData:CacheData){
  return cacheData
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@CacheConfig, @Caching&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@CacheConfig : 클래스 단위로 캐시설정을 할 때 사용한다. (클래스 내의 모든 메서드에 설정이 적용된다.)&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;1656&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sznMS/btrKHZJlogO/JJbMVkS7HU3PiwVonSxta0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sznMS/btrKHZJlogO/JJbMVkS7HU3PiwVonSxta0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sznMS/btrKHZJlogO/JJbMVkS7HU3PiwVonSxta0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsznMS%2FbtrKHZJlogO%2FJJbMVkS7HU3PiwVonSxta0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1656&quot; height=&quot;386&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Caching : 여러 개의 캐싱 동작(@Cacheable, @CacheEvict, @CachePut)을 하나로 묶을 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1661625053273&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@CacheConfig(cacheNames= &quot;books&quot;, cacheManager=&quot;redisCacheManager&quot;)
public class CustomerDataService {

    // 메서드에 따로 적지않아도, @CacheConfig에 적은 내용이 포함된다.
    @Cacheable // == @Cacheable(cacheName=&quot;books&quot;, cacheManager=&quot;redisCacheManager&quot;)
    public String getAddress(Customer customer) {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661625057310&quot; class=&quot;less&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Caching(evict = { 
  @CacheEvict(&quot;addresses&quot;), 
  @CacheEvict(value=&quot;directory&quot;, key=&quot;#customer.name&quot;) })
public String getAddress(Customer customer) { /* ... */ }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj;♂️ @Cacheable(key=&quot;..&quot;)는 생략해도 사용이 가능하긴 합니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천하는 방법은 아니지만, 특정 필드를 key로 사용하지 않고, 그냥 생략해서 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1661625790036&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 깔-끔
@Cacheable(&quot;books&quot;)
public Book findBook(ISBN isbn) { ... }&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;이 경우 스프링의 SimpleKeyGenerator에 의해 아래와 같이 Cache Key를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;SimpleKeyGenreator, SimpleKey 소스코드&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상당히 심플하다. 이게 전부이다&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjoGJn/btrKHc96FSb/eBW74a49VWKOgKAPXQzKPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjoGJn/btrKHc96FSb/eBW74a49VWKOgKAPXQzKPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjoGJn/btrKHc96FSb/eBW74a49VWKOgKAPXQzKPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjoGJn%2FbtrKHc96FSb%2FeBW74a49VWKOgKAPXQzKPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;876&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleKey는 아래와 같이 구현되어있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Dbm3/btrKK9qI4oV/bpKYTott4wMX6JbkDWtU6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Dbm3/btrKK9qI4oV/bpKYTott4wMX6JbkDWtU6K/img.png&quot; data-alt=&quot;class SimpleKey 의 생성자와 메서드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Dbm3/btrKK9qI4oV/bpKYTott4wMX6JbkDWtU6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Dbm3%2FbtrKK9qI4oV%2FbpKYTott4wMX6JbkDWtU6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1700&quot; height=&quot;846&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;class SimpleKey 의 생성자와 메서드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 파라메타가 없는 경우 : SimpleKey.empty()&lt;/li&gt;
&lt;li&gt;메서드 파라메타가 1개인 경우 : 해당 파라메타 값(그대로 박음, SimpleKey로 감싸지 않는다)&lt;/li&gt;
&lt;li&gt;메서드 파라메타가 여러개인 경우 : SimpleKey(..파라메타들..)&lt;span style=&quot;color: #0593d3;&quot;&gt; (* 모든 파라메타의 hashcode 값을 이용해 하나의 복합키로 만 듦)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천하는 방법은 아니지만 이는 KeyGenerator 인터페이스를 직접 구현하고 빈으로 등록해서 이를 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;KeyGenerator 구현 코드&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1661626318477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Bean
  public KeyGenerator keyGenerator() {
    return new CustomKeyGenerator();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661626321370&quot; class=&quot;monkey&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomKeyGenerator implements KeyGenerator {

  @Override
  public Object generate(Object target, Method method, Object... params) {
    StringBuilder keyBuilder = new StringBuilder();
    keyBuilder.append(method.getName());
    keyBuilder.append(SimpleKeyGenerator.generateKey(params));
    return keyBuilder.toString();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;* 스프링에서 캐시 키를 비교할 때, &lt;b&gt;hashcode()만 사용&lt;/b&gt;하고 equals()를 사용하지 않습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;자바의 hashcode는 32비트 메모리 주소기반으로 생성됩니다. 즉 64비트 운영체제 사용 시 아주 낮은 확률로 해시충돌이 발생가능합니다. 해당 이슈(&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/14870&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SPR-10237&lt;/a&gt;) 때문에 스프링 4.0에서는 DefaultKeyGenerator 를 제거하고 SimpleKeyGenerator 로 변경하였습니다.&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;1480&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MtSR8/btrKIx6IiXW/UY01nV7jySOf7vmtSwSbTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MtSR8/btrKIx6IiXW/UY01nV7jySOf7vmtSwSbTk/img.png&quot; data-alt=&quot;SimpleKey(...)에 할당된 파라메타가 여러개인 경우, SimpleKey.readObject 를 사용해서 hashcode를 모든 파라메타의 복합키로 재정의하도록 구현되어있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MtSR8/btrKIx6IiXW/UY01nV7jySOf7vmtSwSbTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMtSR8%2FbtrKIx6IiXW%2FUY01nV7jySOf7vmtSwSbTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1480&quot; height=&quot;254&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SimpleKey(...)에 할당된 파라메타가 여러개인 경우, SimpleKey.readObject 를 사용해서 hashcode를 모든 파라메타의 복합키로 재정의하도록 구현되어있다.&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;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj;♂️ 어노테이션을 커스텀해서 사용할 수도 있습니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 원래 스프링에서 제공하는 기능이긴 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1661628632974&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames=&quot;books&quot;, key=&quot;#isbn&quot;)
public @interface SlowService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661628648649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SlowService // == @Cacheable(cacheNames=&quot;books&quot;, key=&quot;#isbn&quot;)
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;span&gt; CacheManager 가 여러개 일 때 (캐시 저장소를 여러 개를 쓸 때)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;@EnableCaching 으로 스프링 캐시를 활성화 시키면, CacheManager 타입의 빈을 조회하여 기본 매니저로 등록합니다.&lt;br /&gt;이 때 등록된 CacheManager 빈 타입이 여러 개라면 캐시 활성화 도중 에러가 발생합니다. ( NoUniqueBeanDefinitionException )&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661626900309&quot; class=&quot;less&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

    @Override
    @Bean // ex) EHCacheManager
    public CacheManager cacheManager() { ... CacheManager A }

    @Bean // ex) RedisCacheManager
    public CacheManager bCacheManager() { ... CacheManager B }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;해결책1. @Primary&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;추천하는 방법은 아니지만, 뭐 스프링 컨테이너의 @Primary를 사용하면 해결되긴 합니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661626894864&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching
public class MultipleCacheManagerConfig {

    @Bean
    @Primary
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager(&quot;customers&quot;, &quot;orders&quot;);
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager(&quot;customerOrders&quot;, &quot;orderprice&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 기본 캐시매니저는 @Primary 가 사용되고, 필요할  때 아래와 같이 캐시매니저 빈 이름을 명시해주면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661626998747&quot; class=&quot;reasonml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(cacheNames = &quot;customers&quot;) // @Primary CacheManager 사용됨
public Customer getCustomerDetail(Integer customerId) {
    return customerDetailRepository.getCustomerDetail(customerId);
}

// 다른 캐시매니저 사용됨
@Cacheable(cacheNames = &quot;customerOrders&quot;, cacheManager = &quot;alternateCacheManager&quot;)
public List&amp;lt;Order&amp;gt; getCustomerOrders(Integer customerId) {
    return customerDetailRepository.getCustomerOrders(customerId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;해결책2(추천). CachingConfigurerSupport&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스프링 @Configuration에 해당 Configurer 객체를 상속받아서 구현하면 됩니다. 이는 해결책 1과 동일하게 사용 가능합니다.&lt;br /&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;@Primary 가 없는데도 CacheManager 여러 개 등록시 에러가 뜨지 않는 이유 -&amp;gt; CacheResolver가 생성되었기 때문&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 @EnableCaching을 사용했을 때, 스프링 컨테이너를 디버깅해보면 쉽게 알 수 있습니다.&lt;br /&gt;스프링 CacheManager를 등록할 때, 만약 CacheResolver가 존재한다면 기본 CacheManager를 조회-등록하지 않습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UKG9s/btrKHUnxR1M/AsPEo9daWHOHZRsfEe2BK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UKG9s/btrKHUnxR1M/AsPEo9daWHOHZRsfEe2BK0/img.png&quot; data-alt=&quot;getCacheResolver 가 null이 아니라면 캐시매니저를 아예 조회하지 않습니다. 그래서 에러가 안터져요&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UKG9s/btrKHUnxR1M/AsPEo9daWHOHZRsfEe2BK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUKG9s%2FbtrKHUnxR1M%2FAsPEo9daWHOHZRsfEe2BK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;981&quot; height=&quot;469&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;getCacheResolver 가 null이 아니라면 캐시매니저를 아예 조회하지 않습니다. 그래서 에러가 안터져요&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CacheResolver 는 조건에 따라 다른 캐시매니저를 등록하고 싶을 떄, 사용자가 해당 인터페이스를 커스텀해서 만들 수 있습니다.&lt;br /&gt;CachingConfigurerSupport 를 상속받으면, @Configuration 객체가 스프링 빈으로 등록될 때 Configurer가 동작하면서 CacheResolver를 추가합니다.&lt;br /&gt;&lt;br /&gt;조금 더 정확히는 아래와 같은 과정을 거쳐서 스프링 인터셉터로 CacheResolver가 생성되는데, 그냥 넘어갑시다 ㅋㅋ&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&gt;ProxyCachingConfiguration 설정 클래스가 실행되는 과정중에 상위 클래스인&amp;nbsp;&lt;/span&gt;AbstractCachingConfiguration 에 있는 setConfigurers 메소드가 실행이 된다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;setConfigurers 메소드에서&amp;nbsp;&lt;/span&gt;useCachingConfigurer 메소드를 호출하는데, 이 때 파라미터로 받은 CachingConfigurer 객체 에서 cacheManager 를 가져와서 this.cacheManager 에 주입한다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;ProxyCachingConfiguration 설정 클래스에 있는&amp;nbsp;&lt;/span&gt;BeanFactoryCacheOperationSourceAdvisor Bean이 등록되는 과정에서 CacheInterceptor Bean이 추가로 등록된다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;CacheInterceptor Bean이 등록되는 과정에서 cacheResolver가 등록이 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;cacheResolver가 등록이 되었기 때문에 getCacheResolver 메소드가 null이 아니므로 Bean을 찾지 않게 되고 에러가 나지 않게 되는 것이다.&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zocqk/btrKHxMVcwu/jaAY35fYFJy0aynnwpQElk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zocqk/btrKHxMVcwu/jaAY35fYFJy0aynnwpQElk/img.png&quot; data-alt=&quot;기본으로 사용할 객체들을 지정할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zocqk/btrKHxMVcwu/jaAY35fYFJy0aynnwpQElk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzocqk%2FbtrKHxMVcwu%2FjaAY35fYFJy0aynnwpQElk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;493&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기본으로 사용할 객체들을 지정할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1661627039099&quot; class=&quot;scala&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {

    // CachingConfigurerSupport.cacheManager()를 상속했음 ㅋ @Primary 생략가능
    @Override
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager(&quot;customers&quot;, &quot;orders&quot;);
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager(&quot;customerOrders&quot;, &quot;orderprice&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책3. CompositeCacheManager&amp;nbsp;사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 CacheManager 구현체중에는, 아예 캐시매니저를 여러 개 등록할 수 있는 구현체도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1661627448148&quot; class=&quot;reasonml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public CacheManager cacheManager(net.sf.ehcache.CacheManager cm, javax.cache.CacheManager jcm) {
  CompositeCacheManager cacheManager = new CompositeCacheManager();
  
  List&amp;lt;CacheManager&amp;gt; managers = new ArrayList&amp;lt;CacheManager&amp;gt;();
  managers.add(new JCacheCacheManager(jcm));
  managers.add(new EhCacheCacheManager(cm))
  managers.add(new RedisCacheManager(redisTemplate()));
  
  cacheManager.setCacheManagers(managers); // 개별 캐시 매니저 추가
  return cacheManager;
}&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;br /&gt;예를 들어 메서드에서 요청한 캐시매니저가 등록되지 않았다면 NoOpCacheManager가 실행되도록 만들 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1661627522580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TestConfiguration
public class TestConfig {
  @Autowired
  private CacheManager jdkCache; // 예시

  @Autowired
  private CacheManager guavaCache; // 예시

  @Bean
  @Primary
  public CacheManager cacheManager() {
    CompositeCacheManager manager = new CompositeCacheManager(jdkCache, guavaCache);

    // CompositeCacheManager에 포함된 CacheManager 외
    // 모든 캐시 요청을 NoOpCache로 보냄 (여기에선 jdkCache, guavaCache 외의 모든 캐시)
    // NoOpCache는 캐시가 없는 것과 동일하게 동작
    manager.setFallbackToNoOpCache(true);

    return manager;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결책4&lt;span&gt;(추천)&lt;/span&gt;. CacheResolver 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 4.1 부터는 @Cacheable에 아무런 설정 값을 적지 않더라도 캐시매니저를 알아서 찾아줍니다.&lt;br /&gt;이는 cacheManager=&quot;..&quot; 값이 없다면 기본으로 등록된 SimpleCacheResolver가 동작하여 CacheManager를 찾아주기 때문인데, 우리는 이를 커스텀해서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 메서드 이름에 따라 다른 캐시가 동작하도록 하고 싶다면, 아래와 같이 구현할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1661627723628&quot; class=&quot;aspectj&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MultipleCacheResolver implements CacheResolver {
    
    private final CacheManager simpleCacheManager;
    private final CacheManager caffeineCacheManager;    
    private static final String ORDER_CACHE = &quot;orders&quot;;    
    private static final String ORDER_PRICE_CACHE = &quot;orderprice&quot;;
    
    public MultipleCacheResolver(CacheManager simpleCacheManager,CacheManager caffeineCacheManager) {
        this.simpleCacheManager = simpleCacheManager;
        this.caffeineCacheManager=caffeineCacheManager;
        
    }

    // resolveCaches 메서드를 재정의하면 된다.
    @Override
    public Collection&amp;lt;? extends Cache&amp;gt; resolveCaches(CacheOperationInvocationContext&amp;lt;?&amp;gt; context) {
        Collection&amp;lt;Cache&amp;gt; caches = new ArrayList&amp;lt;Cache&amp;gt;();
        
        // 해당 코드는 context 파라메타를 이용해 해당 메서드 이름을 받아와서 비교한다.
        if (&quot;getOrderDetail&quot;.equals(context.getMethod().getName())) {
            caches.add(caffeineCacheManager.getCache(ORDER_CACHE));
        } else {
            caches.add(simpleCacheManager.getCache(ORDER_PRICE_CACHE));
        }
        return caches;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이를 해결책3과 동일하게 기본 CacheResolver로 등록해주면 됩니다.&lt;br /&gt;CacheResolver가 등록 된 경우, @Cacheable에 설정 값이 없을 때 CacheResolver를 거쳐서 캐시 매니저를 찾게됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1661627780641&quot; class=&quot;scala&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {

    @Override
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager(&quot;customers&quot;, &quot;orders&quot;);
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager(&quot;customerOrders&quot;, &quot;orderprice&quot;);
    }

    // 이제 CacheManager를 명시하지 않으면 해당 CacheResolver가 동작하여 찾아준다.
    @Override
    @Bean
    public CacheResolver cacheResolver() {
        return new MultipleCacheResolver(alternateCacheManager(), cacheManager());
    }
}&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;@Cacheable( .. 설정 ..) 에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 설정을 다 생략했는데 등록된 CacheResovler가 없으면 기본 CacheManager를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;span&gt;설정을 다 생략했는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;등록된 CacheResolver가 있다면, CacheResolver가 동작합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 3. 특정  CacheManager를 사용하고 싶다면, @Cacheable(cacheManager=&quot;...&quot;)로 명시해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 특정 CacheResolver를 사용하고 싶다면, @Cacheable(cacheResolver=&quot;...&quot;)로 명시해주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1661628139058&quot; class=&quot;reasonml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(cacheNames = &quot;orders&quot;, cacheResolver = &quot;cacheResolver&quot;)
public Order getOrderDetail(Integer orderId) {
    return orderDetailRepository.getOrderDetail(orderId);
}

@Cacheable(cacheNames = &quot;orderprice&quot;, cacheResolver = &quot;cacheResolver&quot;)
public double getOrderPrice(Integer orderId) {
    return orderDetailRepository.getOrderPrice(orderId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제는 @Cacheable( cacheManager=&quot;..&quot;, cacheResovler=&quot;..&quot;) 이렇게 둘 다 적었을 때 무엇이 동작할지 모른다는건데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 캐시 초기화(@EnableCaching) 시점에 스프링 컨테이너가 예외를 띄워줍니다. 둘 중 하나만 적어야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&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;2458&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XU0EM/btrKF6JvaHe/kKC0pWBi3j7NXpavsDrRI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XU0EM/btrKF6JvaHe/kKC0pWBi3j7NXpavsDrRI0/img.png&quot; data-alt=&quot;스프링 빈 생성시점에 예외가 발생하며, IllegalStateException 에서 해당 어노테이션이 쓰인 메서드 위치까지 알려준다 ㅎ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XU0EM/btrKF6JvaHe/kKC0pWBi3j7NXpavsDrRI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXU0EM%2FbtrKF6JvaHe%2FkKC0pWBi3j7NXpavsDrRI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2458&quot; height=&quot;580&quot; data-origin-width=&quot;2458&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스프링 빈 생성시점에 예외가 발생하며, IllegalStateException 에서 해당 어노테이션이 쓰인 메서드 위치까지 알려준다 ㅎ&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;span&gt; &lt;/span&gt;&amp;nbsp;Redis&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스는 &lt;a href=&quot;https://spring.io/projects/spring-data-redis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring-Data-Redis&lt;/a&gt; 프로젝트가 별도로 존재해서, 스프링에서 설정을 쉽게 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1661628776250&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// 스프링5 webflux 기반에서 사용하는 경우 추가 (아직 spring-data-redis에 통합되지 않음)
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'&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;Spring-Data-Redis에서는&amp;nbsp; Redis Client로 Lettuce와 Jedis를 제공해준다.&lt;br /&gt;2022년 기준 설정의 편의성이나, 성능 , 업데이트 모든 것이 Lettuce가 더 좋기 때문에, 이를 사용하는 것을 권장한다 (&lt;a href=&quot;https://jojoldu.tistory.com/418&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;h3 data-ke-size=&quot;size23&quot;&gt;RedisTemplate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스를 사용하기 위해선 RedisTemplate 객체를 빈으로 등록해주어야한다.&lt;br /&gt; 각종 설정값들은 RedisTemplate에 직접 코드로 넣어도 되지만, 아래와 같은 설정값으로 따로 관리하는게 좋다.&lt;br /&gt;&lt;a href=&quot;https://zetawiki.com/wiki/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8_%EB%8D%B0%EC%9D%B4%ED%84%B0_%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Boot Data 관련 프로퍼티 정리글&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661630552799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml
spring:
  redis:
    host: localhost
    port: 6379&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 302px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 19px;&quot;&gt;spring.redis.database&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 19px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 19px;&quot;&gt;커넥션 팩토리에 사용되는 데이터베이스 인덱스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 35px;&quot;&gt;spring.redis.host&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 35px;&quot;&gt;localhost&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 35px;&quot;&gt;레디스 서버 호스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 19px;&quot;&gt;spring.redis.password&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 19px;&quot;&gt;레디스 서버 로그인 패스워드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 35px;&quot;&gt;spring.redis.pool.max-active&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 35px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 35px;&quot;&gt;pool에 할당될 수 있는 커넥션 최대수 (음수로 하면 무제한)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 19px;&quot;&gt;spring.redis.pool.max-idle&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 19px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 19px;&quot;&gt;pool의 &quot;idle&quot; 커넥션 최대수 (음수로 하면 무제한)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 38px;&quot;&gt;spring.redis.pool.max-wait&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 38px;&quot;&gt;-1&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 38px;&quot;&gt;pool이 바닥났을 때 예외발생 전에 커넥션 할당 차단의 최대 시간 (단위: 밀리세컨드, 음수는 무제한 차단)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 19px;&quot;&gt;spring.redis.pool.min-idle&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 19px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 19px;&quot;&gt;풀에서 관리하는 idle 커넥션의 최소 수 대상 (양수일 때만 유효)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 19px;&quot;&gt;spring.redis.port&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 19px;&quot;&gt;6379&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 19px;&quot;&gt;레디스 서버 포트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 35px;&quot;&gt;spring.redis.sentinel.master&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 35px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 35px;&quot;&gt;레디스 서버 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 35px;&quot;&gt;spring.redis.sentinel.nodes&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 35px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 35px;&quot;&gt;호스트:포트 쌍 목록 (콤마로 구분)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 23.1395%; height: 19px;&quot;&gt;spring.redis.timeout&lt;/td&gt;
&lt;td style=&quot;width: 8.72093%; height: 19px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 68.0233%; height: 19px;&quot;&gt;커넥션 타임아웃 (단위: 밀리세컨드)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisProperties 객체를 사용하는데, 이는 별건 아니고 application.yml 설정 값을 불러오는 편의 객체이다.&lt;/p&gt;
&lt;pre id=&quot;code_1661630292118&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// org.springframework.boot.autoconfigure.data.redis.RedisProperties
@ConfigurationProperties(prefix=&quot;spring.redis&quot;)
public class RedisProperties {...}

// 물론 RedisProperties 객체를 사용하지 않아도 설정파일은 읽을 수 있다.
@Value(&quot;${spring.redis.host}&quot;)
private String host;

@Value(&quot;${spring.redis.port}&quot;)
private int port;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정 값을 이용하여 RedisTemplate 빈을 구성해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1661630042026&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {
    private final RedisProperties redisProperties;

    @Bean // Redis와 Connection을 생성하는 객체를 정의한다. 여기에서는 Lettuce Redis Client 사용
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

    
    @Bean // 스프링에서 제공하는 RedisTemplate을 정의한다. 위에서 정의한 ConnectionFactory를 사용한다
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(){
        RedisTemplate&amp;lt;String,Object&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        //redisTemplate.setKeySerializer(new StringRedisSerializer());    //key serializer
        //redisTemplate.setValueSerializer(new StringRedisSerializer());  //value serializer
        //redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        
        // 위의 설정들을 한번에 설정하고 싶다면 DefaultSerializer 를 등록해주면 된다
        //redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}&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;Redis Serializer 커스텀&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Redis는 Serializer를 별도로 등록하지 않는 경우, key-value를 둘 다 Byte[] 으로 저장해버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisTemplate의 기본값은 &lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;JdkSerializationRedisSerializer  이다.&lt;br /&gt;- 이는 자바의 Serializable 객체를 구현해야만 직렬화가 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;- 기본값을 사용하는 경우 자바가 아닌 다른 언어에서는 Redis 저장소의 값을 읽을 수 없다&lt;br /&gt;- 쓸모 없는 자바의 기본 Object class의 정보까지 Redis에 저장해서 비효율적이다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;그래서 보통 아래 3가지 Serializer를 많이 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;1. GenericJackson2JsonRedisSerializer&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;별도의 클래스 타입을 지정해주지 않아도, 객체를 Json 으로 직렬화해준다.&lt;br /&gt;다만 @class  에 Class 정의 경로를 박아버려서, 데이터를 꺼내올 때 같은 디렉토리에 해당 DTO가 반드시 존재해야하는 단점이 있다.&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;1034&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LAolZ/btrKHw1zg1l/zonK6nufK9e2KDta9wOENk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LAolZ/btrKHw1zg1l/zonK6nufK9e2KDta9wOENk/img.png&quot; data-alt=&quot;com.ssg.api.item.domain.Item 경로를 박아버린다... 이런..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LAolZ/btrKHw1zg1l/zonK6nufK9e2KDta9wOENk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLAolZ%2FbtrKHw1zg1l%2FzonK6nufK9e2KDta9wOENk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;326&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;com.ssg.api.item.domain.Item 경로를 박아버린다... 이런..&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;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;2. Jackson2JsonRedisSerializer&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;특정 클래스 타입을 명시해줘야한다. 그 대신 @class 정보를 데이터에 포함하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;다만 클래스 타입별로 Serializer를 별도로 생성해줘야하는데, cache Dto 가지는 API들이 각자 고유한 RedisTemplate을 따로 들고있어야한다.. ㅠ&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661632637685&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer(value.getClass()));&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: #212529;&quot;&gt;&lt;b&gt;3. StringRedisSerializer + ObjectMapper&lt;/b&gt;&lt;br /&gt;SpringBoot-Redis에 포함된 StringRedisSerializer는 그냥 모든 key-value를 문자열로 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;그리고 값(value)중에 문자열로 변환이 불가능한 객체를 저장할 땐, 아래와 같이 ObjectMapper를 커스텀하여 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class RedisConfig {

  @Bean
  fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory {
    return LettuceConnectionFactory(redisProperties.host, redisProperties.port)
  }

  @Bean
  fun redisObjectMapper(): ObjectMapper = ObjectMapper()
    .registerModule(kotlinModule())
    .registerModule(JavaTimeModule())
    .activateDefaultTyping(
      BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
      ObjectMapper.DefaultTyping.EVERYTHING
    )


  @Bean
  fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate&amp;lt;String, Any?&amp;gt; {
    val redisTemplate = RedisTemplate&amp;lt;String, Any?&amp;gt;()
    redisTemplate.setConnectionFactory(redisConnectionFactory)
    redisTemplate.keySerializer = StringRedisSerializer()
    // GenericJackson2JsonRedisSerializer에 ObjectMapper를 커스텀
    redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer(redisObjectMapper())
    return redisTemplate
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;Redis 사용하기 1 - RedisTemplate 직접 사용&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;참고로 레디스는 단순 스트링 뿐 아니라, 여러가지 자료구조를 제공해주는데 이는 redisTemplate의 아래 메서드로 사용할 수 있다.&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;1482&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vj3aD/btrKHdnESOz/L74ROBNxlt8GS0KArhKTq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vj3aD/btrKHdnESOz/L74ROBNxlt8GS0KArhKTq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vj3aD/btrKHdnESOz/L74ROBNxlt8GS0KArhKTq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVj3aD%2FbtrKHdnESOz%2FL74ROBNxlt8GS0KArhKTq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1482&quot; height=&quot;604&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1661633313752&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 대충 이렇게 사용하면 된다.
object CacheUtil {

  fun setValues(
    redisTemplate: RedisTemplate&amp;lt;String, Any?&amp;gt;,
    prefix: String,
    key: String,
    data: String,
    timeout: Long,
    timeUnit: TimeUnit
  ) {
    redisTemplate.opsForValue().set(&quot;$prefix::$key&quot;, data, timeout, timeUnit)
  }

  fun getValuesOrNull(redisTemplate: RedisTemplate&amp;lt;String, Any?&amp;gt;, prefix: String, key: String): Any? {
    return redisTemplate.opsForValue()[&quot;$prefix::$key&quot;]
  }

  fun deleteValues(redisTemplate: RedisTemplate&amp;lt;String, Any?&amp;gt;, prefix: String, key: String) {
    redisTemplate.delete(&quot;$prefix::$key&quot;)
  }

  fun getRedisTtl(redisTemplate: RedisTemplate&amp;lt;String, Any?&amp;gt;, prefix: String, key: String): Long? {
    return redisTemplate.getExpire(&quot;$prefix::$key&quot;)
  }

  fun setRedisTtl(redisTemplate: RedisTemplate&amp;lt;String, Any?&amp;gt;, prefix: String, key: String, timeout: Long) {
    redisTemplate.expire(&quot;$prefix::$key&quot;, Duration.ofSeconds(timeout))
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 사용하기2 - RedisRepositories (비추천)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권장하는 방법은 아니지만, Redis를 마치 JPA처럼 사용할 수 있다. 참고로 내부적으로 RedisTemplate을 사용하는건 동일하다.&lt;/p&gt;
&lt;pre id=&quot;code_1661633420164&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableRedisRepositories // 추가
public class RedisConfig {
    /*.. 이하 동일 .. */
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661633475843&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RedisHash(value = &quot;refreshToken&quot;)
public class RefreshToken {
    @Id // org.springframework.data.annotation.Id
    private String userId;
    private String refreshToken;

    @TimeToLive
    private long expiredTime;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661633534010&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Spring.Data의 CrudRepository를 상속받은 Repository 인터페이스를 정의한다.
public interface RefreshTokenRedisRepository extends CrudRepository&amp;lt;RefreshToken, String&amp;gt; {
}

// 마치 JPA 처럼 사용할 수 있다.
@Service
public class AdminUserRedisService {
    private final RefreshTokenRedisRepository refreshTokenRedisRepository;

    @Override
    public RefreshToken save(RefreshToken adminUserToken) {
        return refreshTokenRedisRepository.save(adminUserToken);
    }

    @Override
    public RefreshToken findById(String userId) {
        return refreshTokenRedisRepository.findById(userId).get();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661633646889&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class AdminUserRedisServiceTest {

    @Autowired
    private AdminUserRedisService adminUserRedisService;

    @DisplayName(&quot;token redis 저장 success&quot;)
    @Test
    void tokenSave(){
        //given
        RefreshToken token = RefreshToken.builder()
                .userId(&quot;test&quot;)
                .refreshToken(&quot;test_token&quot;)
                .expiredTime(60)
                .build();
                
        //when
        RefreshToken refreshToken = adminUserRedisService.save(token);
        
        //then
        RefreshToken findToken = adminUserRedisService.findById(token.getUserId());
        assertEquals(refreshToken.getRefreshToken(), findToken.getRefreshToken());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 사용하기3 - 스프링 캐시 추상화  CacheManager&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisTemplate을 생성하는 과정은 위와 동일하다. 보통 관리하기 편하게 RedisConfig를 별도로 분리해서 지정하곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팁을 하나 주자면, @ConditionalOnProperty 어노테이션을 이용해 yml 설정에 redis 정보가 있을때만 등록하게 할 수 있다.&lt;br /&gt;&lt;a href=&quot;https://github.com/kwj1270/TIL_FIRST_SPRINGBOOT2/blob/master/02%20%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%20%ED%99%98%EA%B2%BD%20%EC%84%A4%EC%A0%95.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링부트의 조건부 Configuration&amp;nbsp;&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1661633842923&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration // spring.cache.type: redis 일 때 해당 설정을 등록한다, 만약 설정값이 아예 없는 경우 생성하지 않는다.
@ConditionalOnProperty(name = [&quot;spring.cache.type&quot;], havingValue = &quot;redis&quot;, matchIfMissing = false)
class RedisConfig {

  @Bean
  fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory {
    return LettuceConnectionFactory(redisProperties.host, redisProperties.port)
  }

  @Bean // 참고로 그냥 String으로 저장할거면, objectMapper를 커스텀할 필요는 없다 ㅎ
  fun redisObjectMapper(): ObjectMapper = ObjectMapper()
    .registerModule(kotlinModule())
    .registerModule(JavaTimeModule())
    .activateDefaultTyping(
      BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
      ObjectMapper.DefaultTyping.EVERYTHING
    )


  @Bean
  fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate&amp;lt;String, Any?&amp;gt; {
    val redisTemplate = RedisTemplate&amp;lt;String, Any?&amp;gt;()
    redisTemplate.setConnectionFactory(redisConnectionFactory)
    redisTemplate.keySerializer = StringRedisSerializer()
    // GenericJackson2JsonRedisSerializer에 ObjectMapper를 커스텀
    redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer(redisObjectMapper())
    return redisTemplate
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성한 Redis 객체들을 이용하여 CacheManager를 등록해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1661668442078&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Bean  // RedisCacheManager 빈
  public CacheManager cacheManager(RedisTemplate redisTemplate) { 
      return new RedisCacheManager(redisTemplate); // Redis템플릿의 인스턴스를 생성자에 전달하여, 생성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;b&gt;CacheManager만 사용할 경우, RedisTemplate를 꼭 만들 필요는 없다.&lt;/b&gt; 아래와 같이 빌더로 RedisTemplate 의 설정과 비슷하게 만들 수 있다. (RedisCacheConfiguration)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching
class CacheConfig {

    // spring.cache.type: redis 가 아니라면, 생성하지 않는다. (아예 값이 없어도 생성하지 않는다)
    @ConditionalOnProperty(name = [&quot;spring.cache.type&quot;], havingValue = &quot;redis&quot;, matchIfMissing = false)
    @Bean
    fun cacheManager(
       redisConnectionFactory: RedisConnectionFactory,
       objectMapper:ObjectMapper
    ): CacheManager {
      
      val redisConfiguration:RedisCacheConfiguration = RedisCacheConfiguration
      // 해당 캐시매니저가 사용할 기본 설정
      .defaultCacheConfig()
        // 1. 캐시 유효기간(Time to live) 설정
        .entryTtl(Duration.ofSeconds(3600))
        
        // 2. 캐시 이름에 prefix:: 를 자동으로 붙여줌.
        .computePrefixWith(CacheKeyPrefix.simple())
        
        // 3. key 직렬화에 StringRedisSerializer 사용
        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
        
        // 4. value 직렬화에 GenericJackson2JsonRedisSerializer(커스텀한 ObjectMapper) 사용
        .serializeValuesWith(
          RedisSerializationContext.SerializationPair.fromSerializer(
            GenericJackson2JsonRedisSerializer(objectMapper)
          )
        )

      // 앞에서 생성한 redisConnectionFactory 와 여기서 만든 redisConfiguration을 등록함 
      return RedisCacheManager.RedisCacheManagerBuilder
        .fromConnectionFactory(redisConnectionFactory)
        .cacheDefaults(redisConfiguration)
        .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Caffeine&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/java-caching-caffeine&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Caffeine&lt;/a&gt;은 사용법이 단순한 로컬 캐시(메모리 캐시)의 하나로, 자바의 ConcurrentMap 보다 속도가 빠른걸로 유명하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;771&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci3TgW/btrKF1OXxd3/z2FC4V7HBPamluO7Xx5sok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci3TgW/btrKF1OXxd3/z2FC4V7HBPamluO7Xx5sok/img.png&quot; data-alt=&quot;참고로 EHCache도 자바에서 많이 사용하는 로컬 캐시중 하나이다. 서버 분산캐시, 비동기, 디스크저장등 기능은 많으나 성능은 카페인이 우세하다.( https://www.ehcache.org/documentation/3.1/clustered-cache.html )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci3TgW/btrKF1OXxd3/z2FC4V7HBPamluO7Xx5sok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci3TgW%2FbtrKF1OXxd3%2Fz2FC4V7HBPamluO7Xx5sok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;771&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;771&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참고로 EHCache도 자바에서 많이 사용하는 로컬 캐시중 하나이다. 서버 분산캐시, 비동기, 디스크저장등 기능은 많으나 성능은 카페인이 우세하다.( https://www.ehcache.org/documentation/3.1/clustered-cache.html )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;레디스 설정을 봤으면 느꼈겠지만 캐시 구현체 설정이 귀찮은거지 CacheManager 설정은 별거없다 ㅎ&lt;/p&gt;
&lt;pre id=&quot;code_1661635228047&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스프링 로컬 cache를 위한 설정
implementation 'org.springframework.boot:spring-boot-starter-cache'
// caffeine
implementation 'com.github.ben-manes.caffeine:caffeine'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1661664341384&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableCaching
@Configuration
public class CacheConfig {

    @Bean // 카페인 객체를 생성한다. 물론 꼭 빈으로 등록할 필요는 없다.
    public Caffeine caffeineConfig() {
        return Caffeine
                .newBuilder()
                 .recordStats() // 캐시 통계 기록
                 .expireAfterWrite(60, TimeUnit.MINUTES) // .expireAfterWrite(java.time.Duration.ofMinutes(60L))
                 .maximumSize(5)
    }

    @Bean
    public CacheManager cacheManager(Caffeine caffeine) {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔ &lt;span style=&quot;color: #ef5369;&quot;&gt;initialCapacity&lt;/span&gt;: 내부&amp;nbsp;해시&amp;nbsp;테이블의&amp;nbsp;최소한의&amp;nbsp;크기를&amp;nbsp;설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;maximumSize&lt;/span&gt;: 캐시에 포함할 수 있는 최대 엔트리 수를 지정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;maximumWeight&lt;/span&gt;: 캐시에 포함할 수 있는 엔트리의 최대 크기를 지정합니다. (*  maximumSize와 함께 지정할 수 없다, 둘 중 하나만)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;expireAfterAccess&lt;/span&gt;: (캐시가 생성된 후 or 대체되거나 마지막으로 읽은 후) 특정 기간이 경과하면 캐시에서 자동으로 제거&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;expireAfterWrite&lt;/span&gt;: (항목이 생성된 후 or 바뀐 후) 특정 기간이 지나면 각 항목이 캐시에서 자동으로 제거&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;refreshAfterWrite: &lt;span&gt;캐시가 생성되거나 마지막으로 업데이트된 후 지정된 시간 간격으로 캐시를 새로고침(갱신) 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;weakKeys: 키를 weak reference로 지정합니다. (GC에서 회수됨)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;weakValues: Value를 weak reference로 지정합니다. (GC에서 회수됨)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;softValues: Value를&amp;nbsp;soft reference로 지정합니다. (메모리가 가득 찼을 때 GC에서 회수됨)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔&amp;nbsp;&lt;span style=&quot;color: #ef5369;&quot;&gt;recordStats&lt;/span&gt;: 캐시에 대한 Statics를 적용합니다.&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;참고로 recordStats()는 아래와 같은 캐시 통계 객체를 생성한다. 이는 Cache.stats() 메서드로 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1661664567486&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// com.github.benmanes.caffeine.cache.Cache 인터페이스
Cache.stats() // CacheStats 를 반환한다.&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1661664578161&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class CacheStats {

    // ... 
    private final long hitCount;
    private final long missCount;
    private final long loadSuccessCount;
    private final long loadFailureCount;
    private final long totalLoadTime;
    private final long evictionCount;
    private final long evictionWeight;
    // ... 
    public double hitRate() {/* ... */}
    public long evictionCount() {/* ... */}
    public double averageLoadPenalty() {/* ... */}
    // ... 
}&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;캐시 설정 꿀팁1 - ENUM 이용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 설정값들을 Configuration에서 숫자로 박는 건, 유지보수나 관리 측면에서 좋지 못하다.&lt;br /&gt;아래와 같이 설정값들을 가지고 있는 CacheType 객체를 만들고, 이를 Enum으로 들고있자.&lt;/p&gt;
&lt;pre id=&quot;code_1661665494361&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public enum CacheType {

  ARTISTS(&quot;artists&quot;, 5 * 60, 10000), // cachename: artists
  ARTIST_INFO(&quot;artistInfo&quot;, 24 * 60 * 60, 10000); // cachename: artistInfo

  CacheType(String cacheName, int expiredAfterWrite, int maximumSize) {
    this.cacheName = cacheName;
    this.expiredAfterWrite = expiredAfterWrite;
    this.maximumSize = maximumSize;
  }

  private String cacheName;
  private int expiredAfterWrite;
  private int maximumSize;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 설정 꿀팁2- 설정(yml) 읽어서 이용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한걸음 더 나아가서, 설정값들을 자바 객체가 아닌 스프링 프로퍼티로 일괄적으로 관리하여 읽어오면 더 깔끔하다.&lt;/p&gt;
&lt;pre id=&quot;code_1661665737140&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;my:
  project:
    cache-manager:
      type: REDIS
      caches:
        - cacheName: accessToken
          expireAfterWrite: 1800
          
        - cacheName: loginOtp
          expireAfterWrite: 300
          
      type: CAFFEINE
      caches:
        - cacheName: test
          expireAfterWrite: 180
          maximumSize: 5
          
        - cacheName: channels
          expireAfterWrite: 180
          maximumSize: 1
          
        - cacheName: categoryContents
          expireAfterWrite: 1800
          maximumSize: 30
          
        - cacheName: newContentIds
          expireAfterWrite: 1800
          maximumSize: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 이 설정을 쉽게 읽을 수 있게 스프링 프로퍼티 객체를 따로 만든다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// yml 프로퍼티를 객체로 읽어오는 어노태이션
@ConstructorBinding
@ConfigurationProperties(prefix = &quot;my.project.cache-manager&quot;) // 프로퍼티 값 불러오기
data class MyCacheProperties(
  val type: String,
  val caches: List&amp;lt;ArBookCache&amp;gt;? = null
) {
  data class MyCache(
    val cacheName: String,
    val expireAfterWrite: Long,
    val maximumSize: Long? = null
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 프로퍼티를 이용하여 실제 설정 값을 코드가 몰라도, 등록할 수 있게 추상화한다.&lt;/p&gt;
&lt;pre id=&quot;code_1661667234716&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching // 주의 - ConfigurationProperties를 사용할땐, Enable 어노테이션을 추가해야 생성된다.
@EnableConfigurationProperties(MyCacheProperties::class)
class CachingConfig() {
    
    @Bean
    fun cacheManager(myCacheProperties: MyCacheProperties): CacheManager{
        val cacheProperties:List&amp;lt;MyCacheProperties.MyCache&amp;gt; = myCacheProperties.caches
         ?: return NoOpCacheManager() // yml 설정값이 없는 경우, 캐시를 적용하지 않도록 만듦
         
        if(cacheProperties.type == &quot;CAFFEINE&quot;){
           val caches:List&amp;lt;CaffeineCache&amp;gt; = cacheProperties.map{
             CaffeineCache( it.cacheName, // it == MyCacheProperties.MyCache
                            Caffeine.newBuilder()
                            .expireAfterWrite(it.expireAfterWrite.seconds.toJavaDuration())
                            .maximumSize(property.maximumSize ?: 1)
                            .build()
                            )
        }
        
        /*.. 이하 CacheManager 생성부분 생략 ..*/

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 예제일 뿐이고, 위에서 살짝 언급한 @CondtionalOnProperty 같은&lt;a href=&quot;https://github.com/kwj1270/TIL_FIRST_SPRINGBOOT2/blob/master/02%20%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%20%ED%99%98%EA%B2%BD%20%EC%84%A4%EC%A0%95.md&quot;&gt;스프링부트의 조건부 빈 등록&amp;nbsp;&lt;/a&gt;과 함께 사용하면, yml 설정만으로 동작을 변경하도록 추상화 할 수 있다.&lt;/p&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://jiwondev.tistory.com/282#head13&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위에서 언급한 CacheConfigurerSupport, CacheResolver&lt;/a&gt;등을 사용하여 적절하게 처리하자&lt;/p&gt;</description>
      <category>  2025/  Spring Framework</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/282</guid>
      <comments>https://jiwondev.tistory.com/282#entry282comment</comments>
      <pubDate>Sun, 26 May 2024 13:04:46 +0900</pubDate>
    </item>
    <item>
      <title>대규모 분산 환경 key-value 저장소 설계</title>
      <link>https://jiwondev.tistory.com/300</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3e4QA/btsFckRKCja/b5Tj03llzk1gkOxDy4J2FK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3e4QA/btsFckRKCja/b5Tj03llzk1gkOxDy4J2FK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3e4QA/btsFckRKCja/b5Tj03llzk1gkOxDy4J2FK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3e4QA%2FbtsFckRKCja%2Fb5Tj03llzk1gkOxDy4J2FK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;427&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bytebytego.com/courses/system-design-interview/design-a-key-value-store&quot;&gt;https://bytebytego.com/courses/system-design-interview/design-a-key-value-store&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1708520276463&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;ByteByteGo | Technical Interview Prep&quot; data-og-description=&quot;Everything you need to take your system design skill to the next level&quot; data-og-host=&quot;bytebytego.com&quot; data-og-source-url=&quot;https://bytebytego.com/courses/system-design-interview/design-a-key-value-store&quot; data-og-url=&quot;https://bytebytego.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cMc0Z1/hyVmYslTND/zWPMvMcB7cYtufsSSgbkKK/img.png?width=2396&amp;amp;height=1248&amp;amp;face=0_0_2396_1248&quot;&gt;&lt;a href=&quot;https://bytebytego.com/courses/system-design-interview/design-a-key-value-store&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bytebytego.com/courses/system-design-interview/design-a-key-value-store&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cMc0Z1/hyVmYslTND/zWPMvMcB7cYtufsSSgbkKK/img.png?width=2396&amp;amp;height=1248&amp;amp;face=0_0_2396_1248');&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;ByteByteGo | Technical Interview Prep&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Everything you need to take your system design skill to the next level&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bytebytego.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;전통적인 관계형 DB와 다르게 상황에 따라 key-value 형태로 저장이 필요한 경우가 있다. 대표적인 서비스로는 AWS DynamoDB, Memcached, Redis 같은 것들이 있으며 큰 틀에서는 아래와 같은 특징을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key: 유일한 값이어야한다. 값의 종류는 상관없다. 숫자형, 해쉬값, 의미있는 텍스트등.&lt;/li&gt;
&lt;li&gt;value: 관계형 DB와는 다르게 문자열, 리스트, 객체 무엇이든 될 수 있다.&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;943&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JPcmE/btsE7qlTQnS/kWs8VAkw2jdHfwAKKvhi7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JPcmE/btsE7qlTQnS/kWs8VAkw2jdHfwAKKvhi7K/img.png&quot; data-alt=&quot;Redis에서 지원하는 value 값의 형태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JPcmE/btsE7qlTQnS/kWs8VAkw2jdHfwAKKvhi7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJPcmE%2FbtsE7qlTQnS%2FkWs8VAkw2jdHfwAKKvhi7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;236&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis에서 지원하는 value 값의 형태&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;세상에 완벽한 설계는 없다. 우린 언제나 데이터의 일관성과 가용성 사이에 적절하게 트레이드 오프를 고려해야한다. 이 글에서도 아래와 같이 실제 대규모 비즈니스에서 사용가능할 정도의 제약을 두고 시작하고자 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key-value 쌍의 크기는 10KB 이하&lt;/li&gt;
&lt;li&gt;수백만 이상의 사용자가 만들어내는 대용량 데이터를 처리할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;높은 가용성 (High Availablity): 시스템의 응답은 빨라야한다. 설령 장애가 발생했더라도.&lt;/li&gt;
&lt;li&gt;높은 확장성 (High Scalabilty): 트래픽 양이 늘었을 때 손쉽게 서버 를 확장할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;자동 스케일링(Automatic scaling): 트래픽 양이 증가함에 따라 서버의 증설/삭제가 자동으로 이루어질 수 있어야한다.&lt;/li&gt;
&lt;li&gt;데이터의 일관성을 조절할 수 있어야 한다. (Tunable consistency)&lt;/li&gt;
&lt;li&gt;응답 지연시간이 짧아야 한다. (Low latency)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ 단일 서버 key-value 저장소&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2️⃣ 분산 서버 key-value 저장소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 서버를 아무리 scale-up해서 사용한다해도 언젠가 부족한 때가 찾아오기 마련이다. 이 때는 서버를 분산시켜 사용해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 분산 해시테이블이라고도 불리는데 이를 설계할 때는 트레이드 오프의 기본인 CAP(Consitency, Availabilty, Partition Tolerance ) 를 고려해야한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yj8Ie/btsFbfwGxM8/JQ6qxIw1oUb4OofqFvcKhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yj8Ie/btsFbfwGxM8/JQ6qxIw1oUb4OofqFvcKhK/img.png&quot; data-alt=&quot;서버를 구성할  때 세가지중 두 가지 속성만 충족할 수 있다. 나머지 하나는 희생될 수 밖에 없다. (* 그중 실 비즈니스에서 분단 내성은 필수)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yj8Ie/btsFbfwGxM8/JQ6qxIw1oUb4OofqFvcKhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyj8Ie%2FbtsFbfwGxM8%2FJQ6qxIw1oUb4OofqFvcKhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1614&quot; height=&quot;1144&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;1144&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: #0593d3;&quot;&gt;&amp;nbsp;* P의 정확한 정의는 분산 네트워크간 통신과정에서 일어나는 메시지 손실을 얼마나 허용할지 규칙을 정하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CP 시스템: 일관성, 파티션 감내를 지원&lt;/li&gt;
&lt;li&gt;AP 시스템: 가용성과 파티션 감내를 지원&lt;/li&gt;
&lt;li&gt;CA 시스템: 일관성과 가용성을 지원하는 저장소. 다만 대부분의 분산 환경 비즈니스에서 분단 내성을 포기하는건 불가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스 운영에서 특정 파티션의 장애는 피할 수 없다. 이 때 일관성과 가용성 중 어떤 것을 우선적으로 챙겨야할지 고민해야한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cc3qbZ/btsE8O7PYTT/Mvsotk6OCqu2ki4XHQjXb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cc3qbZ/btsE8O7PYTT/Mvsotk6OCqu2ki4XHQjXb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cc3qbZ/btsE8O7PYTT/Mvsotk6OCqu2ki4XHQjXb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcc3qbZ%2FbtsE8O7PYTT%2FMvsotk6OCqu2ki4XHQjXb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;219&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일관성(CP)이 중요하다면 쓰기 연산을 중지시키면 파티션간 데이터 불일치를 막을 수 있다.  단 서비스 가용성은 많이 낮아진다.&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;온라인 뱅킹 같이 계좌의 최신정보를 출력하지 못하는 경우를 생각해보자. 이 때는 가용성보다 일관성이 훨씬 중요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;가용성(AP)이 중요하다면 옛날 데이터를 잘못 반환하더라도 쓰기 및 읽기를 계속 허용해야한다.&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;페이스북 같은 커뮤니티 서비스라면 댓글이 잠깐 잘못나오더라도 서비스 가용성 과 사용자 경험을 챙기는게 훨씬 중요하다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 분산환경 key-value 저장소를 구현할 때에는 여러 가지 요소를 고려해야하는데, 각 요소들을 하나씩 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #212529;&quot;&gt;Data partition (데이터 파티션)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #212529;&quot;&gt;Data  replication (데이터 다중화)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #212529;&quot;&gt;Data consistency (데이터 일관성, 일관성 모델)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #212529;&quot;&gt;Inconsistency resolution (일관성 불일치 해소)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #212529;&quot;&gt;Handling failures (장애 처리, 장애 감지)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #212529;&quot;&gt;System architecture diagram (아키텍처 다이어그램 설계)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# Data partition (데이터 파티션)&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;/li&gt;
&lt;li&gt;서버(노드)가 추가, 삭제되더라도 데이터를 많이 이동하지 않아도 되는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 앞에서 배운 안정해시를 통해 해결할 수 있다.  안정해시를 사용하면 추가적으로 자동 확장(Auto Scaling)이 쉬워지고 각 서버의 용량에 맞게 설정을 다양화(heterogeneity) 할 수 있다. &lt;a href=&quot;https://jiwondev.tistory.com/299&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.02.18 - [ 기본 지식/CS 기본지식] - 안정 해시(Consistent Hashing)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1708532199731&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;안정 해시(Consistent Hashing)&quot; data-og-description=&quot;  해시를 사용해 요청을 분산하는 방법 N개의 캐시 서버가 있을 때 부하를 나누는 보편적인 방법으로는 해시 함수를 많이 사용한다. MD5 (2^128): 속도가 보안보다 중요한 어플리케이션. SHA-256 에 &quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/299&quot; data-og-url=&quot;https://jiwondev.tistory.com/299&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/T1KSg/hyVqoXiwx2/tYIYFCrGj1qmVMMw0of5g0/img.png?width=463&amp;amp;height=438&amp;amp;face=0_0_463_438,https://scrap.kakaocdn.net/dn/cgXpAm/hyVmWnO4ig/C9kILztdSs8SQAEGmvGW40/img.png?width=463&amp;amp;height=438&amp;amp;face=0_0_463_438,https://scrap.kakaocdn.net/dn/gggVJ/hyVqiityfG/9fJO7g5hTIUnikLztm0jO0/img.png?width=1324&amp;amp;height=542&amp;amp;face=0_0_1324_542&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/299&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/299&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/T1KSg/hyVqoXiwx2/tYIYFCrGj1qmVMMw0of5g0/img.png?width=463&amp;amp;height=438&amp;amp;face=0_0_463_438,https://scrap.kakaocdn.net/dn/cgXpAm/hyVmWnO4ig/C9kILztdSs8SQAEGmvGW40/img.png?width=463&amp;amp;height=438&amp;amp;face=0_0_463_438,https://scrap.kakaocdn.net/dn/gggVJ/hyVqiityfG/9fJO7g5hTIUnikLztm0jO0/img.png?width=1324&amp;amp;height=542&amp;amp;face=0_0_1324_542');&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;안정 해시(Consistent Hashing)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  해시를 사용해 요청을 분산하는 방법 N개의 캐시 서버가 있을 때 부하를 나누는 보편적인 방법으로는 해시 함수를 많이 사용한다. MD5 (2^128): 속도가 보안보다 중요한 어플리케이션. SHA-256 에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# Data replication (데이터 다중화)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 서버를 아무리 철저하게 관리한다고 해도 높은 가용성과 안정성을 얻긴 어렵다. 이를 확보하려면 비동기적인 다중화가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정해시를 사용하는 예를 들어본다면 시계 방향으로 원을 순회하면서 중복되지 않는 첫 N개 서버에 데이터 사본을 보관하면 된다. 또한 정전, 네트워크 이슈, 자연재해등을 방지하기위해 각 물리서버는 다른 데이터 센터에 보관하도록 구성하면 된다. (이는 AWS 같은 클라우드를 사용한다면 쉽게 구성 가능하다.)&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;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU5Bwv/btsE9G2R9DB/IlL48ZXEV7O2stJUdHgZm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU5Bwv/btsE9G2R9DB/IlL48ZXEV7O2stJUdHgZm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU5Bwv/btsE9G2R9DB/IlL48ZXEV7O2stJUdHgZm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU5Bwv%2FbtsE9G2R9DB%2FIlL48ZXEV7O2stJUdHgZm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;247&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# Data consistency (데이터 일관성)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서버에 데이터를 다중화하기 때문에 적절한 로직을 사용해 데이터 가 동기화가 되어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Quorum Consensus(정족수 합의) 프로토콜을 사용하면 읽기/쓰기 연산에 아래와 같이 일관성을 보장할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;N: 사본개수&lt;/li&gt;
&lt;li&gt;W: 쓰기 연산에 대한 서버 정족수(* 완료처리를 위한 최소 성공 수)&lt;/li&gt;
&lt;li&gt;R: 읽기 연산에 대한 서버 정족수(* 완료처리를 위한 최소 성공 수)&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;870&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw2vP7/btsE91lrXT3/ztEK3uGgXg4FJt9mZwDJ5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw2vP7/btsE91lrXT3/ztEK3uGgXg4FJt9mZwDJ5k/img.png&quot; data-alt=&quot;클라이언트  요청은 중재자(coordinator)가 받아서 프록시처럼 처리한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw2vP7/btsE91lrXT3/ztEK3uGgXg4FJt9mZwDJ5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw2vP7%2FbtsE91lrXT3%2FztEK3uGgXg4FJt9mZwDJ5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;309&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클라이언트  요청은 중재자(coordinator)가 받아서 프록시처럼 처리한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;W,R,N 을 따로 정의한 이유는 응답 레이턴시와 데이터 일관성 사이에 트레이드 오프를 고려해야하기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;W,R 이 몇 개 안되는 경우 응답 레이턴시는 매우 빠를 것이다. 대신 데이터 일관성은 잠깐 일치하지 않을 수 있다.&lt;/li&gt;
&lt;li&gt;W,R 이 전체 개수와 비슷하다면 응답 레이턴시는 느릴 것이다. 하지만 데이터 일관성은 언제나 같을 것이다.&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;1110&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ninUi/btsFaOTwKzi/J93tpXkLPm1O9dpWZzAxG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ninUi/btsFaOTwKzi/J93tpXkLPm1O9dpWZzAxG0/img.png&quot; data-alt=&quot;정답은 없다. 서비스에 맞춰서 결정해야한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ninUi/btsFaOTwKzi/J93tpXkLPm1O9dpWZzAxG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FninUi%2FbtsFaOTwKzi%2FJ93tpXkLPm1O9dpWZzAxG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;135&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;338&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;&amp;nbsp;&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;강한 일관성(Strong Consistency) : 모든 읽기는 최신 데이터이다. 낡은 데이터를 반환하는 경우는 없다.&lt;/li&gt;
&lt;li&gt;약한 일관성(Weak Consistency) : 읽기 연산은 최신 데이터가 아닐 수 있다. 어느정도 일관성 손실을 감수한다.&lt;/li&gt;
&lt;li&gt;최종 일관성(Eventual Consistency) : 읽기 연산은 최신 데이터가 아닐 수 있으나 시간이 지나면 모든 사본에 일관성이 맞춰진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 같은 경우엔 강한 일관성을, 동영상 스트리밍서비스는 약한 일관성을, key-value 인 다이나모, 카산드라 같은경우에는 최종 일관성 모델을 선택하고 있다.&lt;/p&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;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pMH2G/btsE8DFl2R5/xrBBAcantOqmXDKUpKjKc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pMH2G/btsE8DFl2R5/xrBBAcantOqmXDKUpKjKc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pMH2G/btsE8DFl2R5/xrBBAcantOqmXDKUpKjKc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpMH2G%2FbtsE8DFl2R5%2FxrBBAcantOqmXDKUpKjKc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;352&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 일관성등에서 일관성을 해소하는 방법 중 하나는, 데이터 버저닝(versioning)과 벡터 시계(vector clock)을 사용하는 방법이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버저닝은&amp;nbsp;데이터&amp;nbsp;변경&amp;nbsp;시마다&amp;nbsp;데이터의&amp;nbsp;새로운&amp;nbsp;버전을&amp;nbsp;만드는&amp;nbsp;것이다.&amp;nbsp;각&amp;nbsp;버전의&amp;nbsp;데이터는&amp;nbsp;불변하다&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;1488&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpy78n/btsFckKVDwF/N4RaKuH8aGumVV2uBShMT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpy78n/btsFckKVDwF/N4RaKuH8aGumVV2uBShMT1/img.png&quot; data-alt=&quot;충돌나면 그 다음 요청자(클라이언트)가 이를 직접 git merge 해서 해결하는 것과 비슷한 원리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpy78n/btsFckKVDwF/N4RaKuH8aGumVV2uBShMT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpy78n%2FbtsFckKVDwF%2FN4RaKuH8aGumVV2uBShMT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1488&quot; height=&quot;752&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;충돌나면 그 다음 요청자(클라이언트)가 이를 직접 git merge 해서 해결하는 것과 비슷한 원리&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자(클라이언트)가 충돌을 감지하고 해소해야한다. 클라이언트 구현이 복잡해진다&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;Last Write Wins&amp;nbsp; 같은 규칙을 정하고 timestamp에 기반해서 충돌을 해결하는 로직을 구현해야한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;수정이 잦을 경우, 이론상 [서버:버전]의 쌍이 엄청나게 많이 늘어날 수 있다.&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;다행히 AWS 다이나모 문헌에 따르면 실제 서비스에서 [서버:버전]쌍이 많아져서 문제가 된 적은 없다고 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# Handling failures (장애 감지, 처리)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;대규모 시스템에서 장애는 그저 불가피하기만 한 것이 아니라 아주 흔하게 벌어지는 사건이다. 먼저 장애 감지 기법을 살펴보고 뒤에서 장애 해소에 대해 알아보도록 하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서 가장 단순한 방법은 전부 다 연결해 멀티캐스팅(multicasting)  으로 장애를 감지하는 것이다. 당연히 이는 비효율적이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctVb6w/btsFcmaTClp/d3Rlv4x4y0SWvrt7NdPq6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctVb6w/btsFcmaTClp/d3Rlv4x4y0SWvrt7NdPq6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctVb6w/btsFcmaTClp/d3Rlv4x4y0SWvrt7NdPq6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctVb6w%2FbtsFcmaTClp%2Fd3Rlv4x4y0SWvrt7NdPq6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;323&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나은 방법으로는 가십 프로토콜(gossip protocol)같은 분산형 장애 감지 솔루션을 사용할 수 있다. 이는 아래와 같이 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 노드는 멤버십 목록을 유지한다. 멤버십 목록은&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 [memberId, Heartbeat counter] 쌍의 목록이다.&lt;/li&gt;
&lt;li&gt;각 노드는 주기적으로 자신의 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Heartbeat counter&lt;/span&gt; 증가시킨다.&lt;/li&gt;
&lt;li&gt;각 노드는 무작위로 선정된 노드들에게 주기적으로 자신의 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Heartbeat counter&lt;/span&gt;&amp;nbsp;목록을 보낸다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Heartbeat counter&lt;/span&gt;&amp;nbsp;목록을 받은 노드는 멤버십 목록을 최신값으로 갱신한다.&lt;/li&gt;
&lt;li&gt;어떤 멤버의 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Heartbeat counter&lt;/span&gt; 값이 지정된 시간동안 갱신되지 않으면 (= 특정 서버의 counter 리스트를 어디에도 받지 못했다면) 해당 멤버는 장애 상태인 것으로 간주된다.&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;1474&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ThNix/btsFbgvz6wl/XT8kHnz8qfBKPfQogm39T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ThNix/btsFbgvz6wl/XT8kHnz8qfBKPfQogm39T1/img.png&quot; data-alt=&quot;counter가 오랫동안 증가되지않으면, 이를 발견한 모든 노드는 해당ID를 장애 노드로 표시한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ThNix/btsFbgvz6wl/XT8kHnz8qfBKPfQogm39T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FThNix%2FbtsFbgvz6wl%2FXT8kHnz8qfBKPfQogm39T1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;238&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;counter가 오랫동안 증가되지않으면, 이를 발견한 모든 노드는 해당ID를 장애 노드로 표시한다.&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;1️⃣ 가십 프로토콜로 발견 -&amp;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;엄격한 정족수(strict quorum): 전체 읽기/쓰기 연산을 금지시켜버린다. 장애 발생시 가용성은 최악이 된다.&lt;/li&gt;
&lt;li&gt;느슨한 정족수( sloppy quorum): 쓰기 연산용 W개의 정상노드, 읽기 연산용 R개의 정상노드만 사용한다. 나머지 서버는 무시한다.&lt;br /&gt;이후 장애 서버가 복구되었을 때 변경사항(hint)을 일괄 반영하여 데이터 정합성을 보장한다.&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;798&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uwplb/btsE7rZpolr/djYvUx7tyWa9EnpEAQewt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uwplb/btsE7rZpolr/djYvUx7tyWa9EnpEAQewt0/img.png&quot; data-alt=&quot;장애 해결이후 데이터를 옮길 수 있게 별도로 기록(hint)한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uwplb/btsE7rZpolr/djYvUx7tyWa9EnpEAQewt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuwplb%2FbtsE7rZpolr%2FdjYvUx7tyWa9EnpEAQewt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;268&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;장애 해결이후 데이터를 옮길 수 있게 별도로 기록(hint)한다.&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;2️⃣ 가십 프로토콜로 발견 -&amp;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;반-엔트로피 프로토콜 (anti-entropy protocol) 을 사용할 수 있다. 이는 사본을 비교하여 최신 버전으로 갱신하는 방법이다.&lt;/p&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;아래와 같이 머클트리(자식 노드들의 레이블로부터 계산된 해시값을 테이블로 붙여두는 트리)를 구성한다.&lt;/li&gt;
&lt;li&gt;이후 왼쪽 자식-&amp;gt;오른쪽 자식으로 BFS 탐색을 하며 root 노드 해시와 비교한다.&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-origin-width=&quot;1512&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A3OIx/btsE99jnbBk/EsvPhpKvA7jiu6A5kVx1W1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A3OIx/btsE99jnbBk/EsvPhpKvA7jiu6A5kVx1W1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A3OIx/btsE99jnbBk/EsvPhpKvA7jiu6A5kVx1W1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA3OIx%2FbtsE99jnbBk%2FEsvPhpKvA7jiu6A5kVx1W1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1512&quot; height=&quot;830&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머클 트리:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;/u&gt;&lt;u&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Merkle_tree&quot;&gt;https://en.wikipedia.org/wiki/Merkle_tree&lt;/a&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;카산드라 아키텍처:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;/u&gt;&lt;u&gt;&lt;a href=&quot;https://cassandra.apache.org/doc/latest/architecture/&quot;&gt;https://cassandra.apache.org/doc/latest/architecture/&lt;/a&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SStable:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;/u&gt;&lt;u&gt;&lt;a href=&quot;https://www.igvita.com/2012/02/06/sstable-and-log-structured-storage-leveldb/&quot;&gt;https://www.igvita.com/2012/02/06/sstable-and-log-structured-storage-leveldb/&lt;/a&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;블룸 필터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;/u&gt;&lt;u&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Bloom_filter&quot;&gt;https://en.wikipedia.org/wiki/Bloom_filter&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ 특정 데이터센터의 장애&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 자연재해에 가까우므로 프로토콜로 극복할 수 없다. 초기 설계부터 데이터 센터를 다중화하여 서버를 구성하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# System architecture diagram (아키텍처 다이어그램 설계)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 여러가지를 고려해보았으니 이제 시스템을 구현하기위해 큰그림, 즉 아키텍처를 그릴 수 있을 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;키-값 저장소가 제공하는 API&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;get(key)와&lt;span&gt;&amp;nbsp;&lt;/span&gt;put(key, value)로 통신한다.&lt;/li&gt;
&lt;li&gt;중재자는 클라이언트에게 키-값 저장소에 대한 프락시 역할을 하는 노드다.&lt;/li&gt;
&lt;li&gt;노드는 안정해시의 해시링 위에 분포한다.&lt;/li&gt;
&lt;li&gt;Client API 진입점 및 분산 환경에서 필요한 언급한 요소( 장애감지, 데이터 정합성, 다중화, 복구..) 를 다 지원해야한다.&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;1256&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rwQSX/btsE9IzySa9/vcfV1wOZseyIrZOJ9uOu81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rwQSX/btsE9IzySa9/vcfV1wOZseyIrZOJ9uOu81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rwQSX/btsE9IzySa9/vcfV1wOZseyIrZOJ9uOu81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrwQSX%2FbtsE9IzySa9%2FvcfV1wOZseyIrZOJ9uOu81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;283&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 key-value 저장소중 하나인 카산드라는 아래와 같이 쓰기와 읽기를 구현한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmsa7l/btsE9GPlyPh/HFwhi8H7TadXLxCTbLnHl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmsa7l/btsE9GPlyPh/HFwhi8H7TadXLxCTbLnHl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmsa7l/btsE9GPlyPh/HFwhi8H7TadXLxCTbLnHl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmsa7l%2FbtsE9GPlyPh%2FHFwhi8H7TadXLxCTbLnHl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1490&quot; height=&quot;580&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  부록 -  AWS Dynamo DB 가 설계된 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 전세계적인 서비스를 운영할 때 필요한 고가용성의 분산 key-value 저장소가 필요했고 이를 구현하기 위한 논문(&lt;a href=&quot;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Dynamo: Amazon&amp;rsquo;s Highly Available Key-value Store&lt;/a&gt;)을 게제 후 만든 서비스가 DynamoDB이다. &lt;span style=&quot;color: #0593d3;&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;물론 심심해서 Dynomo를 만든 건 아니 다. AWS는 기존에는 Oracle DB로 운영하였으나, 서비스가 전세계적으로 확장되며 더 이상 관계형DB로는 감당할 수가 없었기에 직접 분산환경에 적절한 고가용성 DB를 설계하고 유지보수하기로 결정하였다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# 어떤 과정을 거쳐 만들어졌을까&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경 Key-Value 저장소인 Dynomo가 탄생하게 된 배경은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Oracle 데이터베이스 사용 패턴을 조사 -&amp;gt; 스케일 아웃 처리를 위해 JOIN 자체를 사용하지 않는 경우가 많았다.&lt;/li&gt;
&lt;li&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;1806&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beuQyI/btsFaEXOiV5/xxNzgQnYA3TQ2OS1aQVww0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beuQyI/btsFaEXOiV5/xxNzgQnYA3TQ2OS1aQVww0/img.png&quot; data-alt=&quot;https://changhoi.kim/posts/database/dynamodb-internals-1/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beuQyI/btsFaEXOiV5/xxNzgQnYA3TQ2OS1aQVww0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeuQyI%2FbtsFaEXOiV5%2FxxNzgQnYA3TQ2OS1aQVww0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;406&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;1102&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://changhoi.kim/posts/database/dynamodb-internals-1/&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;h3 data-ke-size=&quot;size23&quot;&gt;# 어떤 고민들이 있었을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynomo는 네트워크 장애가 발생할 것이라는 가정 아래 가용성을 최대록 높이도록 디자인되었다.&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;2206&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUwU0e/btsE9ZHShyk/6KNEpSjR4vmv35kj6SZqKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUwU0e/btsE9ZHShyk/6KNEpSjR4vmv35kj6SZqKK/img.png&quot; data-alt=&quot;읽기 시점의 해결은, 일단 쓰기는 허용하고 이후 복구하는 과정을 거치게된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUwU0e/btsE9ZHShyk/6KNEpSjR4vmv35kj6SZqKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUwU0e%2FbtsE9ZHShyk%2F6KNEpSjR4vmv35kj6SZqKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2206&quot; height=&quot;570&quot; data-origin-width=&quot;2206&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 저장소에서는 DB 트랜잭션처럼 쓰기를 막을 수 없다. 그래서 &quot;항상 쓰기가 가능한&quot; key-value 저장소를 목표로 하게된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언제 해결할 것인가?&lt;br /&gt;쓰기 작업의 가용성을 위해 읽기 작업에서 충돌을 해결한다.&lt;/li&gt;
&lt;li&gt;누가 해결할 것인가?&lt;br /&gt;DB가 아닌 서버 로직을 구현하여 이를 처리한다.&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;노드를 추가/삭제할 때 운영 및 시스템에 최소한의 영향만을 주어야한다.&lt;/li&gt;
&lt;li&gt;단일 노드의 장애가 전파되서는 안된다. 모든 노드가 동일한 역할을 수행해야한다.&lt;/li&gt;
&lt;li&gt;중앙 시스템이 장애로 마비되더라도 P2P 를 통한 분산 컨트롤이 가능한 시스템을 설계해야한다.&lt;/li&gt;
&lt;li&gt;각 노드들은 상황에 따라 효율적으로 다양하게(Heterogeneitry) 구성될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;파티셔닝 ( 노드 추가 / 제거)&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;안정해시 사용&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;트래픽에 따른 서버 증설, 즉 증분 확장성을 쉽게 보장할 수 있음.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;쓰기 고가용성(HA) 구성&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;vector clock 사용&lt;br /&gt;데이터 충돌은 클라이언트가 해결&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;데이터 전파는 Eventual Consistency를 선택함. 쓰기 가용성을 보장받을 수 있음.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;일시적 실패&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;quorum 프로토콜과 Hinted Handoff(복구를 위한 기록) 사용&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;범용적인 시스템을 위한 일시적인 실패 복구 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;멤버십, 장애 탐지&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;gossip 프로토콜 사용&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;각 노드의 동일성을 유지하면서 탈 중앙화 시스템을 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynomo는 안정 해시에 의해 모든 노드에서 쓰기가 가능하다. 즉 Leader 노드라는 개념 자체가 모호하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기를 실패하는 것은 느슨한 정족수 (sloppy quorum)를 이용해 읽기/쓰기가 성공했는지를 판단한다.&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;2018&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXr8S/btsE95nHBLo/JG3L0Ed1csbxNhWW6Jq4lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXr8S/btsE95nHBLo/JG3L0Ed1csbxNhWW6Jq4lK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXr8S/btsE95nHBLo/JG3L0Ed1csbxNhWW6Jq4lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXr8S%2FbtsE95nHBLo%2FJG3L0Ed1csbxNhWW6Jq4lK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;211&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 quorum과는 조금 다른게 Dynamo는 요청을 받아 노드에게 전파해주는 중개자(Coordinator)가 따로 없다. 요청을 보내는 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; 요청을 받아 중개자가 된 노드는 복제해야하는 노드 수 만큼 다른 노드(preference list)에 데이터를 전파해야한다. 이 과정 속에서 실패 여부는 언급한대로 느슨한 정족수(Sloppy Quorum)를 사용해서 성공 응답을 클라이언트에게 반환한다. &lt;span style=&quot;color: #0593d3;&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;1746&quot; data-origin-height=&quot;1140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mu2NX/btsFaN78r0Q/sNSwXOkrlkPjkdOMTKuQd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mu2NX/btsFaN78r0Q/sNSwXOkrlkPjkdOMTKuQd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mu2NX/btsFaN78r0Q/sNSwXOkrlkPjkdOMTKuQd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMu2NX%2FbtsFaN78r0Q%2FsNSwXOkrlkPjkdOMTKuQd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;362&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;1140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용들을 읽어보면 앞에서 언급한 내용들과 비슷한 부분이 많다는 걸 느낄 것이다.&lt;/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;898&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OIgpr/btsE93wD0wx/CmN4CGPvClTd9J3uisK7D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OIgpr/btsE93wD0wx/CmN4CGPvClTd9J3uisK7D0/img.png&quot; data-alt=&quot;앞에서 배운 내용들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OIgpr/btsE93wD0wx/CmN4CGPvClTd9J3uisK7D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOIgpr%2FbtsE93wD0wx%2FCmN4CGPvClTd9J3uisK7D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;436&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;588&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;애초에 Dynomo도 천재 개발자가 짠하고 만들어낸 것이 아니라 전부터 논의되어오던 여러 방법들을 최적화한 결과이기 때문이기도 하다.&lt;/p&gt;</description>
      <category>  2025/CS 기본지식</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/300</guid>
      <comments>https://jiwondev.tistory.com/300#entry300comment</comments>
      <pubDate>Thu, 22 Feb 2024 04:02:08 +0900</pubDate>
    </item>
    <item>
      <title>안정 해시(Consistent Hashing)</title>
      <link>https://jiwondev.tistory.com/299</link>
      <description>&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;463&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clwEcA/btsE1EpiZwV/h9k1r7cZlkgUgRfTt0Ehh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clwEcA/btsE1EpiZwV/h9k1r7cZlkgUgRfTt0Ehh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clwEcA/btsE1EpiZwV/h9k1r7cZlkgUgRfTt0Ehh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclwEcA%2FbtsE1EpiZwV%2Fh9k1r7cZlkgUgRfTt0Ehh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;463&quot; height=&quot;438&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  해시를 사용해 요청을 분산하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N개의 캐시 서버가 있을 때 부하를 나누는 보편적인 방법으로는 해시 함수를 많이 사용한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;MD5 (2^128): 속도가 보안보다 중요한 어플리케이션. SHA-256 에 비해 해시 공간은 작다.&lt;br /&gt;SHA-256 (2^256): 더 긴 해시 크기와 더 강력한 암호화 속성. 속도는 MD5 에 비해 느리다. 매우 큰 해시 공간을 갖기 때문에 충돌이 거의 발생하지 않는다.&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;serverIndex = hash(key) % N (서버개수)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2050&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs2v5Z/btsETkdqADb/Kv11jLLHYRY0kVhkMMmm2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs2v5Z/btsETkdqADb/Kv11jLLHYRY0kVhkMMmm2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs2v5Z/btsETkdqADb/Kv11jLLHYRY0kVhkMMmm2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs2v5Z%2FbtsETkdqADb%2FKv11jLLHYRY0kVhkMMmm2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;243&quot; data-origin-width=&quot;2050&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방법은 모든 상황에서 잘 동작할 것 같지만 서버 개수 (server pool)이 변경되거나 데이터가 한쪽에 몰리게되었을 때 문제가 발생한다. 키를 적절하게 재배치 하지 않는다면 부하가 몰리게 되고 대규모의 캐시 미스(cache miss)가 발생할 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qkw98/btsERywKbN9/z3rjG7Ekn0OyFCFFGrbz0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qkw98/btsERywKbN9/z3rjG7Ekn0OyFCFFGrbz0k/img.png&quot; data-alt=&quot;처음엔 문제가 없었겠지만, 지금은 해시키를 재배치 해주지 않으면 문제가 발생한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qkw98/btsERywKbN9/z3rjG7Ekn0OyFCFFGrbz0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQkw98%2FbtsERywKbN9%2Fz3rjG7Ekn0OyFCFFGrbz0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;265&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;790&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;이를 방지하려면&lt;b&gt; 서버가 추가/삭제 되더라도 key 매핑 변경은 최소화&lt;/b&gt; 되어야 한다. 이 때 사용하는 것이 바로 &quot;안정 해시&quot; 이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;안정해시(Consistent Hashing)&lt;br /&gt;MIT에서 처음 제안 되었으며 해시 테이블의 크기가 변경될 때, 평균적으로 오직&amp;nbsp; k(키의개수) / n(slot의 개수)&amp;nbsp; 개의 키만 재배치하는 기술이다. 안정해시로 구현하지 않는 해시함수들은 데이터를 넣는 슬롯의 수가 줄어거나 늘어나면 많은 키 가 재배치 되야 한다.&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;안정해시는 어떻게 구현할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 해시 함수로 SHA-1을 사용한다고 가정하면, 해시 공간은 0 &amp;lt;= x &amp;lt; 2^160 까지 사용할 수 있다. 이를 시작과 끝을 붙이게되면 원형 리스트로 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2204&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLgaFg/btsETYVvQ4U/xt6LmVrsO4hBp3Yr0bbDr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLgaFg/btsETYVvQ4U/xt6LmVrsO4hBp3Yr0bbDr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLgaFg/btsETYVvQ4U/xt6LmVrsO4hBp3Yr0bbDr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLgaFg%2FbtsETYVvQ4U%2Fxt6LmVrsO4hBp3Yr0bbDr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;229&quot; data-origin-width=&quot;2204&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1707942035720&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ConsistentHash&amp;lt;T&amp;gt;(
    private val hashFunction: (String) -&amp;gt; Long, // 사용할 해시 함수
    private val circle: SortedMap&amp;lt;Long, T&amp;gt; = TreeMap() // 해시 값을 키로 하고 노드를 값으로 하는 정렬된 맵
){ 
   /* ... */ 
}

// 해시 함수의 예제 구현입니다. Java의 MessageDigest를 사용해 문자열의 SHA-1 해시 값을 생성합니다.
fun hashFunction(key: String): Long {
    val md = MessageDigest.getInstance(&quot;SHA-1&quot;)
    return md.digest(key.toByteArray())
             .fold(0L, { acc, byte -&amp;gt; (acc shl 8) + (byte and 0xff) })
}
    
fun main() {
    val consistentHash = ConsistentHash(::hashFunction)
}&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;이렇게 구성한 원형 리스트 에서  서버(IP나 서버 이름) 을 저장하고 사용할 hash key 들을 원하는 어느 지점에나 배치할 수 있게 구성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때 기존의 해시가 아닌 균등 분포 (uniform distribution) 해시 함수를 사용해서 원형에 골고루 분포시킨다.&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;2046&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y8AEX/btsERxYZtmP/rp66q3FBHfktN6E3R6ptTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y8AEX/btsERxYZtmP/rp66q3FBHfktN6E3R6ptTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y8AEX/btsERxYZtmP/rp66q3FBHfktN6E3R6ptTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy8AEX%2FbtsERxYZtmP%2Frp66q3FBHfktN6E3R6ptTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;255&quot; data-origin-width=&quot;2046&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 원형으로 구성하게되면 추가/삭제에서 기존의 키 매핑 변경이 최소화 되도록 구현할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 조회&lt;br /&gt;해당 hash key로부터 시계방향으로 서버를 탐색하여 만난 첫번째 서버를 조회한다.&lt;/li&gt;
&lt;li&gt;서버 추가&lt;br /&gt; 서버를 추가하 더라도 시계방향으로 hash key를 사용했던 기존 1개의 서버만  키가 변경된다. 다른 키는 재배치 되지 않는다.&lt;/li&gt;
&lt;li&gt;서버 삭제&lt;br /&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-origin-width=&quot;2640&quot; data-origin-height=&quot;1468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1PuCk/btsERyQ5hb4/t0mtYmal9gF6tDpJgo3qtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1PuCk/btsERyQ5hb4/t0mtYmal9gF6tDpJgo3qtK/img.png&quot; data-alt=&quot;각 서버(노드) 사이에 있는 key만 재배치하면 된다. 서버가 추가/삭제 되더라도 key 재배치가 최소화 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1PuCk/btsERyQ5hb4/t0mtYmal9gF6tDpJgo3qtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1PuCk%2FbtsERyQ5hb4%2Ft0mtYmal9gF6tDpJgo3qtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2640&quot; height=&quot;1468&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1468&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;각 서버(노드) 사이에 있는 key만 재배치하면 된다. 서버가 추가/삭제 되더라도 key 재배치가 최소화 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707942116094&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ConsistentHash&amp;lt;T&amp;gt;(
    private val hashFunction: (String) -&amp;gt; Long, // 해시 함수
    private val circle: SortedMap&amp;lt;Long, T&amp;gt; = TreeMap() // 해시 값을 키로 하고 노드를 값으로 하는 정렬된 맵
) {
    fun addNode(node: T) {
        // 노드를 해시링에 추가합니다.
        val hash = hashFunction(node.toString())
        circle[hash] = node
    }

    fun removeNode(node: T) {
        // 노드를 해시링에서 제거합니다.
        val hash = hashFunction(node.toString())
        circle.remove(hash)
    }

    fun getNode(key: String): T? {
        // 주어진 키에 대한 노드를 반환합니다.
        if (circle.isEmpty()) return null
        
        val hash = hashFunction(key)
        
        // 주어진 해시 값을 조회하지 못한 경우, 가장 가까운 다음 키를 찾습니다.
        if (!circle.containsKey(hash)) {
            // key 보다 순서가 같거나 큰 서브 맵 생성(tailMap)
            val tailMap = circle.tailMap(hash)
            // 다음 순서가 없다면 한 바퀴 돌아 첫번째 순서(circle.firstKey()) 사용
            hash = tailMap.firstKey() ?: circle.firstKey()
        }
        return circle[hash]
    }
}&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; &amp;nbsp;이렇게 구현한 안정해시는 단점이 없을까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 그림처럼 각 서버 사이의 빈 공간(partition) 을 항상 균등하게 유지하는게 불가능하다.&lt;/li&gt;
&lt;li&gt;균등 분포 해시함수를 사용하더라도 키 분포 자체가 한쪽 빈 공간(partition)에 몰릴 수도 있다.&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;1324&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IDCcW/btsEOSCJRBU/g3a1mJZpouwXyDGctlrrY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IDCcW/btsEOSCJRBU/g3a1mJZpouwXyDGctlrrY0/img.png&quot; data-alt=&quot;그냥 해시를 쓰는 것 보단 낫겠지만, 문제가 완벽하게 해결된 건 아니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IDCcW/btsEOSCJRBU/g3a1mJZpouwXyDGctlrrY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIDCcW%2FbtsEOSCJRBU%2Fg3a1mJZpouwXyDGctlrrY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;273&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;542&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  해결책: 가상 노드(virtual node) 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버(노드)를 1:1로 사용하지 않고, 가상의 노드를 만들어서 사용한다. 서버 1대가 여러개의 가상 노드를 가질 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가상 노드의 개수를 늘림으로서 키의 분포를 균등하게 만들 수 있다.&lt;/li&gt;
&lt;li&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;2668&quot; data-origin-height=&quot;1062&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddglmW/btsETjljQ6C/MKkeU0RYiNTD7HPTZEZkNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddglmW/btsETjljQ6C/MKkeU0RYiNTD7HPTZEZkNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddglmW/btsETjljQ6C/MKkeU0RYiNTD7HPTZEZkNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddglmW%2FbtsETjljQ6C%2FMKkeU0RYiNTD7HPTZEZkNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2668&quot; height=&quot;1062&quot; data-origin-width=&quot;2668&quot; data-origin-height=&quot;1062&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1707942727695&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 가상 노드를 나타내는 클래스
class VirtualNode&amp;lt;T&amp;gt;(
    private val physicalNode: T,
    private val replicaIndex: Int
) {
    fun getPhysicalNode(): T = physicalNode // 물리 노드 (실제 서버)

    fun getKey(): String = &quot;$physicalNode#$replicaIndex&quot; // 가상 노드의 고유 키(해시에 사용)

    fun isVirtualOf(node: T): Boolean = this.physicalNode == node

    override fun toString(): String = getKey()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1707942937463&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ConsistentHash&amp;lt;T&amp;gt;(
    private val hashFunction: (String) -&amp;gt; Long,
    private val numberOfReplicas: Int,
    private val circle: SortedMap&amp;lt;Long, VirtualNode&amp;lt;T&amp;gt;&amp;gt; = TreeMap()
) {
    fun addNode(node: T) {
        for (i in 0 until numberOfReplicas) {
            val virtualNode = VirtualNode(node, i)
            circle[hashFunction(virtualNode.getKey())] = virtualNode
        }
    }

    fun removeNode(node: T) {
        circle.entries.removeIf { it.value.isVirtualOf(node) }
    }

    fun getNode(key: String): T? {
        if (circle.isEmpty()) return null

        var hash = hashFunction(key)
        if (!circle.containsKey(hash)) {
            val tailMap = circle.tailMap(hash)
            hash = tailMap.firstKey() ?: circle.firstKey()
        }
        return circle[hash]?.getPhysicalNode()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성함으로서 아래와 같은 이점을 챙길 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 추가/삭제에도 키 매핑 변경이 최소화된다. cache miss 비율을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;서버 추가/삭제를 굉장히 많이 하더라도 한쪽으로 몰리지 않고 데이터가 균등하게 분포된다.&lt;/li&gt;
&lt;li&gt;위 장점으로 수평적 규모 확장이 쉬워지고 hot spot key (한쪽으로 키가 몰리는 현상)을 최소화 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;1446&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbo0ig/btsETmvySM0/7tTF59nAEFXVbhzaLrdXN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbo0ig/btsETmvySM0/7tTF59nAEFXVbhzaLrdXN0/img.png&quot; data-alt=&quot;실험 출처: https://songkg7.github.io/posts/Consistent-Hashing/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbo0ig/btsETmvySM0/7tTF59nAEFXVbhzaLrdXN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbo0ig%2FbtsETmvySM0%2F7tTF59nAEFXVbhzaLrdXN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;150&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실험 출처: https://songkg7.github.io/posts/Consistent-Hashing/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;라우팅을 만든다고 생각해보자. 여기에서 key 는 request url,  ip 등이 될 것이고 키에 따라 적절한 서버를 찾아주어야한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때 key(요청)에 따라 어디로 라우팅 되었는지 캐싱해둔다. 이후 동일 요청은 라우팅 결정과정을 생략하고 바로 조회한다.&lt;/li&gt;
&lt;li&gt;캐시가 없거나 해당 cache 를 사용했는데 정보가 유효하지 않은 경우 (노드가 변경된 경우) 다시 라우팅 결정과정을 거친다.&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;1798&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTjrgQ/btsETVEuqKr/7oPzDWjglVUNW2Bdjyfmq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTjrgQ/btsETVEuqKr/7oPzDWjglVUNW2Bdjyfmq0/img.png&quot; data-alt=&quot;ChatGPT의 친절한 설명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTjrgQ/btsETVEuqKr/7oPzDWjglVUNW2Bdjyfmq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTjrgQ%2FbtsETVEuqKr%2F7oPzDWjglVUNW2Bdjyfmq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;232&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ChatGPT의 친절한 설명&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;&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;2948&quot; data-origin-height=&quot;1210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GcV9q/btsETlwD1ms/Iy5NkLlK64F6PWQaZdM3y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GcV9q/btsETlwD1ms/Iy5NkLlK64F6PWQaZdM3y1/img.png&quot; data-alt=&quot;그냥 해시 ( https://songkg7.github.io/posts/Consistent-Hashing/ )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GcV9q/btsETlwD1ms/Iy5NkLlK64F6PWQaZdM3y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGcV9q%2FbtsETlwD1ms%2FIy5NkLlK64F6PWQaZdM3y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2948&quot; height=&quot;1210&quot; data-origin-width=&quot;2948&quot; data-origin-height=&quot;1210&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그냥 해시 ( https://songkg7.github.io/posts/Consistent-Hashing/ )&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;&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;2534&quot; data-origin-height=&quot;1154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cudtLx/btsENVNfyMk/njGe0Wfxi45SExrUKOiOR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cudtLx/btsENVNfyMk/njGe0Wfxi45SExrUKOiOR1/img.png&quot; data-alt=&quot;안정해시(노드1:1)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cudtLx/btsENVNfyMk/njGe0Wfxi45SExrUKOiOR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcudtLx%2FbtsENVNfyMk%2FnjGe0Wfxi45SExrUKOiOR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2534&quot; height=&quot;1154&quot; data-origin-width=&quot;2534&quot; data-origin-height=&quot;1154&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안정해시(노드1:1)&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;&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;가상 노드 수를 늘리면 확실히 더 골고루 분산된다. 다만 가상 노드 개수가 어느정도 차게되면 이후 수십만개를 늘려도 결과는 큰 변화가 없다는걸 인지하자. 이것도 트레이드 오프를 고려해야한다.&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-origin-width=&quot;3458&quot; data-origin-height=&quot;1150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGunsz/btsEQgckDGH/exD2HHqZkCyvImivAgy3k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGunsz/btsEQgckDGH/exD2HHqZkCyvImivAgy3k1/img.png&quot; data-alt=&quot;안정해시( 노드1:1 노드1:10 노드1:100 )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGunsz/btsEQgckDGH/exD2HHqZkCyvImivAgy3k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGunsz%2FbtsEQgckDGH%2FexD2HHqZkCyvImivAgy3k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3458&quot; height=&quot;1150&quot; data-origin-width=&quot;3458&quot; data-origin-height=&quot;1150&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안정해시( 노드1:1 노드1:10 노드1:100 )&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;h2 style=&quot;color: #000000; text-align: start;&quot; 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;a href=&quot;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;아마존 다이나모 데이터베이스(DynamoDB)의 파티셔닝 관련 컴포넌트&lt;/a&gt;&lt;br /&gt;Dynamo는 리더가 없는 복제시스템으로 모든 노드에서 쓰기 요청처리가 가능하다. 데이터를 여러 파티션에 분배해서 사용할 때 안정 해시를 이용해 데이터 리밸런싱을 최소화한다. (다이나모가 어떻게 구성되었는지 궁금하다면 &amp;gt;&amp;nbsp; &lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;좋은글1&lt;/a&gt; /&amp;nbsp;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/database/choosing-the-right-dynamodb-partition-key/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;좋은글2&lt;/a&gt; )&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.cs.cornell.edu/Projects/ladis2009/papers/Lakshman-ladis2009.PDF&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;아파치 카산드라(Apache Cassandra) 클러스터에서의 데이터 파티셔닝&lt;/a&gt;&lt;br /&gt;데이터를 클러스터 내의 여러 노드에 분산시키는데 파티셔닝 키를 해싱할 때 안정 해시를 사용한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.discord.com/scaling-elixir-f9b8e1e7c29b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;디스코드(Discord)채팅 어플리케이션 - Elixir를 동시 사용자 오백만명으로 확장한 방법&lt;/a&gt;&lt;br /&gt;새로운 유저가 들어왔을 때, 전 세계에 거 친 서버 클러스터 내에서 채널, 사용자 그룹을 특정 노드에 할당하여 사용한다. 이  때 안정 해시를 사용하여 노드의 추가/삭제에 따른 리밸런싱을 최소화한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.akamai.com/ko/solutions/content-delivery-network&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;아카마이(Akamai) CDN&lt;/a&gt;&lt;br /&gt;사용자의 요청을 전 세계에 분산된 캐시(컨텐츠) 서버에 효율적으로 라우팅한다. 서버가 추가/삭제 되더라도 요청 리밸런싱이 최소화 되도록 안정 해시를 사용한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;매그래프(Meglev) 네트워크 로드 밸런서&lt;/a&gt;&lt;br /&gt;구글이 만든 네트워크  로드밸런서로 트래픽을 균등하게 분산시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1708256216219&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;Choosing the Right DynamoDB Partition Key | Amazon Web Services&quot; data-og-description=&quot;September 2022: This post was reviewed and updated for accuracy. This blog post covers important considerations and strategies for choosing the right partition key for designing a schema that uses Amazon DynamoDB. Choosing the right partition key is an imp&quot; data-og-host=&quot;aws.amazon.com&quot; data-og-source-url=&quot;https://aws.amazon.com/ko/blogs/database/choosing-the-right-dynamodb-partition-key/&quot; data-og-url=&quot;https://aws.amazon.com/blogs/database/choosing-the-right-dynamodb-partition-key/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tzDsX/hyVmV9gCgm/edklpneS0MwZKL3XYwotEK/img.png?width=814&amp;amp;height=495&amp;amp;face=0_0_814_495,https://scrap.kakaocdn.net/dn/UeKYd/hyVjkbLWnv/48UigcMkEkWMlg8a4qpZv0/img.png?width=814&amp;amp;height=495&amp;amp;face=0_0_814_495&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/database/choosing-the-right-dynamodb-partition-key/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aws.amazon.com/ko/blogs/database/choosing-the-right-dynamodb-partition-key/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tzDsX/hyVmV9gCgm/edklpneS0MwZKL3XYwotEK/img.png?width=814&amp;amp;height=495&amp;amp;face=0_0_814_495,https://scrap.kakaocdn.net/dn/UeKYd/hyVjkbLWnv/48UigcMkEkWMlg8a4qpZv0/img.png?width=814&amp;amp;height=495&amp;amp;face=0_0_814_495');&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;Choosing the Right DynamoDB Partition Key | Amazon Web Services&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;September 2022: This post was reviewed and updated for accuracy. This blog post covers important considerations and strategies for choosing the right partition key for designing a schema that uses Amazon DynamoDB. Choosing the right partition key is an imp&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;aws.amazon.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 대규모 케이스가 아니더라도 당장 사용자수가 많아져 DB 스케일업을 고려해야한다면  파티셔닝을할지, 샤딩을 할지 고민하고 한다면 어떤 key를 기준으로 나눠야할지를 고민해보아야한다. &lt;a href=&quot;https://medium.com/miro-engineering/choosing-a-hash-function-to-solve-a-data-sharding-problem-c656259e2b54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MiRO 엔지니어링 - 데이터 샤딩 문제를 해결하기 위한 적절한 해시함수 선택&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;1430&quot; data-origin-height=&quot;1222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dse2jd/btsE42cbD7R/GyqyD6vMPDcrWXdFny81g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dse2jd/btsE42cbD7R/GyqyD6vMPDcrWXdFny81g0/img.png&quot; data-alt=&quot;당연한 말이지만 Hash가 정답이 아닐 수도 있다. 하지만 이런 방법들을 알고 있는게 분명 선택에 도움이 될 것이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dse2jd/btsE42cbD7R/GyqyD6vMPDcrWXdFny81g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdse2jd%2FbtsE42cbD7R%2FGyqyD6vMPDcrWXdFny81g0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;569&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;1222&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;당연한 말이지만 Hash가 정답이 아닐 수도 있다. 하지만 이런 방법들을 알고 있는게 분명 선택에 도움이 될 것이다.&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>  2025/CS 기본지식</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/299</guid>
      <comments>https://jiwondev.tistory.com/299#entry299comment</comments>
      <pubDate>Sun, 18 Feb 2024 21:31:49 +0900</pubDate>
    </item>
    <item>
      <title>주관적인 인프런 강의 리뷰( 김영한님 스프링)</title>
      <link>https://jiwondev.tistory.com/265</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;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt; 인프런 김영한님&lt;/h1&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;앞부분에 원리를 길게 설명하고, 실제 사용법은 후반부에 나오기 때문에 순서대로 강의를 보면 답답할 수 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님이 어떤분인지 궁금하면, &lt;a href=&quot;https://www.youtube.com/watch?v=Pb69UQ6f8n0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;개발바닥 유튜브&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=Pb69UQ6f8n0&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/dYKJDK/hyNwbfP8qi/6ljDyDBih5DuflaTcM0K0K/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=130_74_1168_622&quot; data-video-width=&quot;666&quot; data-video-height=&quot;375&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/Pb69UQ6f8n0&quot; width=&quot;666&quot; height=&quot;375&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;돈 많고 백수라서 시간 빌게이츠라면 모든 강의를 다 구매해서 보시는게 제일 좋습니다. 그게 아니라면 인프런은 거의 매달마다 다양한 이유로 20~30% 할인을 하 니 필요한 강의들을 할인기간에 사시는걸 추천드려요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;자바와 스프링에 익숙하고 웹에 대한 깊이있는 학습을 원해요.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF를 먼저 훑어보면서 너무 기초적인 사용법에 대한 설명은 스킵하면서 보는걸 추천드립니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1️⃣ 모든 개발자를 위한 HTTP&lt;br /&gt;직접 찾아보지않으면 실무에서 크게 배울 일 없는 HTTP 와 웹 서버에 대한 기초지식을 쌓습니다.&lt;/li&gt;
&lt;li&gt;2️⃣ 스프링 MVC 1편&lt;br /&gt;웹 개발 기초와 함께 MVC 프레임워크를 직접 만들어보면서 Spring MVC가 어떤식으로 설계되었는지를 배웁니다.&lt;/li&gt;
&lt;li&gt;3️⃣ JPA 활용 2편&lt;br /&gt;JPA 성능튜닝에 관한 내용입니다. 다만 요약하면 내용이 짧아서 구글에 내용을 검색해서 봐도 무방합니다.&lt;/li&gt;
&lt;li&gt;4️⃣ 스프링 핵심원리 2편&lt;br /&gt;로그 기능을 발전시켜나가며 스프링 AOP와 빈 후처리기가 어떻게 동작하는지에 대해 깊게 배웁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; 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;1️⃣ (무료) 스프링 입문&lt;br /&gt;생각을 비우고 무지성으로 따라하는 걸 추천합니다. 일단 스프링 코드에 익숙해져야 이론이 쉬워집니다.&lt;/li&gt;
&lt;li&gt;2️⃣ 스프링 MVC 1편&lt;br /&gt;기본적인 웹 개발에 대해 배웁니다. 내용을 다 이해하지 못하더라도 스프링 MVC 기능을 하나씩 써보며 익숙해집니다.&lt;/li&gt;
&lt;li&gt;3️⃣ 스프링 핵심원리 기본편&lt;br /&gt;객체지향과 스프링 코어(빈 컨테이너)가 무엇인지 학습합니다.&lt;/li&gt;
&lt;li&gt;4️⃣ 모든 개발자를 위한 HTTP&lt;br /&gt;HTTP와 웹 개발 기초에 대해 학습합니다.&lt;/li&gt;
&lt;li&gt;5️⃣  실전! 스프링 DB 접근기술 2편&lt;br /&gt;실무에서 많이쓰는 DB 기술에 대해 간단한 사용법을 배우고, 스프링 트랜잭션 원리에 대해 학습합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도면 충분합니다. 사용법도 모르는 상태로 이론적인 내용을 배우면 금방 지치기 때문에 실제 프로젝트를 진행하며 코드 작성에 충분히 익숙해진 다음 나머지 강의를 들으시는 걸 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; 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;1️⃣ (무료) 김영한의 자바 입문 - 코드로 시작하는 자바 첫 걸음&lt;br /&gt;스프링부터 보지말고 컴퓨터 언어에 대한 기초를 Java로 배우고, 쉬운 알고리즘 문제를 풀면서 익숙해지는걸 추천드립니다. 참고로 자바 강의는 꼭 영한님 강의가 아니더라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://www.youtube.com/watch?v=oJlCC1DutbA&amp;amp;list=PLW2UjW795-f6xWA2_MUhEVgPauhGl3xIp&quot;&gt;자바의 정석 유튜브&lt;/a&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;와 같이 좋은 강의들이 많습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;2️⃣ (무료) 스프링 입문&lt;br /&gt;생각을 비우고 무지성으로 따라하는 걸 추천합니다. 일단 스프링 코드에 익숙해져야 이론이 쉬워집니다.&lt;/li&gt;
&lt;li&gt;3️⃣ 실전 스프링 부트와 JPA 활용1&lt;br /&gt;어느정도 완성도 있는 프로젝트를 따라 만들어봅니다. 모르는 내용이 있다면 질문/답변이나 구글링을 통해 어떻게든 완주합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도 진행하면서 자바에 익숙해졌다면 그 이후는 위와 동일합니다. 개발을 한번도 해보시지 않았다면 내가 적성에 맞는지도 잘 모르기 때문에 스터디나 토이 프로젝트를 따라 만들어보며 먼저 개발에 익숙해지고  그 이후를 선택하시는걸 추천드려요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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; &amp;nbsp;&amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 입문&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무료&amp;nbsp;강의이고,&amp;nbsp;스프링의&amp;nbsp;모든&amp;nbsp;기능을&amp;nbsp;코드로&amp;nbsp;쳐보면서&amp;nbsp;전체적으로&amp;nbsp;훑어줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모든&amp;nbsp;개발자를&amp;nbsp;위한&amp;nbsp;HTTP&lt;/a&gt;&amp;nbsp;(⭐강추⭐)&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;HTTP 특성과&amp;nbsp; 그에 따른 API 설계방법을 다룹니다. 백엔드 개발자가 아니더라도 들어두면 좋은 강의&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링&amp;nbsp;핵심원리&amp;nbsp;기본편&lt;/a&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;강의가&amp;nbsp;재미있긴&amp;nbsp;하지만,&amp;nbsp;스프링&amp;nbsp;입문자에겐&amp;nbsp;내용이&amp;nbsp;&amp;nbsp;어려울&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;프로젝트를&amp;nbsp;해보고&amp;nbsp;듣는&amp;nbsp;걸&amp;nbsp;추천&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링&amp;nbsp;핵심원리&amp;nbsp;고급편&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링에서&amp;nbsp;사용되는&amp;nbsp;디자인패턴들을&amp;nbsp;'로그를&amp;nbsp;담당하는&amp;nbsp;객체'&amp;nbsp;를&amp;nbsp;한단계씩&amp;nbsp;발전시켜가며&amp;nbsp;배웁니다.&lt;/li&gt;
&lt;li&gt;앞에서 배운 지식을 바탕으로 스프링 AOP, 빈 후처리기의 동작과 원리에 대해 알려줍니다.&lt;/li&gt;
&lt;li&gt;몰라도&amp;nbsp;큰&amp;nbsp;상관없지만,&amp;nbsp;알아두면&amp;nbsp;좋은&amp;nbsp;지식들이며&amp;nbsp;스프링에&amp;nbsp;익숙하지&amp;nbsp;않다면&amp;nbsp;이해하기&amp;nbsp;어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링&amp;nbsp;MVC&amp;nbsp;1편&lt;/a&gt;&amp;nbsp;(⭐강추⭐)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서블릿에&amp;nbsp;대한&amp;nbsp;설명을&amp;nbsp;시작으로,&amp;nbsp;웹&amp;nbsp;서비스를&amp;nbsp;따라&amp;nbsp;만들어봅니다.&lt;/li&gt;
&lt;li&gt;한단계식 코드를 개선시키며 최종적으로 스프링 MVC의 Dispatcher Servlet을 완성시킵니다.&lt;/li&gt;
&lt;li&gt;강의의 기승전결이 완벽합니다.&amp;nbsp;스프링이 아니더라도, 웹 개발을 하며 꼭 필요한 지식들을 함께 배울 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링&amp;nbsp;MVC&amp;nbsp;2편&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링&amp;nbsp;MVC에&amp;nbsp;제공되는&amp;nbsp;기능들을&amp;nbsp;다&amp;nbsp;훑어주며&amp;nbsp;원리를&amp;nbsp;설명해줍니다.&lt;/li&gt;
&lt;li&gt;인터셉터,&amp;nbsp;국제화,&amp;nbsp;Validation,&amp;nbsp;예외처리,&amp;nbsp;타입컨버터,&amp;nbsp;파일업로드&amp;nbsp;등의&amp;nbsp;원리를&amp;nbsp;자세하게&amp;nbsp;설명합니다.&lt;/li&gt;
&lt;li&gt;다만 원리에 비해 스프링이 제공하는 사용법은 간단해서 공부한 뒤 현타가 올 수있습니다.&lt;/li&gt;
&lt;li&gt;이것도&amp;nbsp;스프링에&amp;nbsp;있는&amp;nbsp;기능들을&amp;nbsp;한번씩&amp;nbsp;써보고&amp;nbsp;듣는&amp;nbsp;걸&amp;nbsp;추천&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/ORM-JPA-Basic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPA&amp;nbsp;기본편&lt;/a&gt;&amp;nbsp;(⭐강추⭐)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영한님의&amp;nbsp;JPA&amp;nbsp;책과&amp;nbsp;내용이&amp;nbsp;같습니다.&amp;nbsp;JPA의&amp;nbsp;원리와&amp;nbsp;사용법에&amp;nbsp;대해&amp;nbsp;자세하게&amp;nbsp;다룹니다.&lt;/li&gt;
&lt;li&gt;다만&amp;nbsp;이것도&amp;nbsp;JPA를&amp;nbsp;사용해보고&amp;nbsp;원리를&amp;nbsp;배우는걸&amp;nbsp;추천합니다.&amp;nbsp;처음부터&amp;nbsp;원리를&amp;nbsp;깊게&amp;nbsp;배우면&amp;nbsp;힘듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1#reviews&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPA&amp;nbsp;활용&amp;nbsp;1편&amp;nbsp;(JPA&amp;nbsp;사용)&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA을&amp;nbsp;사용해&amp;nbsp;간단한&amp;nbsp;스프링&amp;nbsp;웹&amp;nbsp;어플리케이션을&amp;nbsp;만듭니다.&lt;/li&gt;
&lt;li&gt;JPA를&amp;nbsp;다양하게&amp;nbsp;활용하지만,&amp;nbsp;원리를&amp;nbsp;기본편만큼&amp;nbsp;자세하게&amp;nbsp;설명&amp;nbsp;하지는&amp;nbsp;않습니다.&lt;/li&gt;
&lt;li&gt;참고로 활용편을 듣고 (혹은 개인 프로젝트에서 JPA를 써보고) 기본편을 들으면 강의 내용이 좀 더 쉽게 이해됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94#curriculum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPA&amp;nbsp;활용&amp;nbsp;2편&amp;nbsp;(JPA&amp;nbsp;성능최적화)&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실무에서&amp;nbsp;JPA사용&amp;nbsp;시&amp;nbsp;자주&amp;nbsp;발생하는&amp;nbsp;실수와&amp;nbsp;JPA&amp;nbsp;내에서&amp;nbsp;성능을&amp;nbsp;올릴&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;방법을&amp;nbsp;설명해줍니다.&lt;/li&gt;
&lt;li&gt;패치조인,&amp;nbsp;DTO로&amp;nbsp;바로&amp;nbsp;조회하기,&amp;nbsp;페이징&amp;nbsp;쿼리&amp;nbsp;최적화등을&amp;nbsp;다룹니다.&lt;/li&gt;
&lt;li&gt;설명은 정말 잘해주시지만, 결론을 정리하면 내용이 얼마 안됩니다. 구글에 강의 내용을 검색해서 봐도 무방합니다.&lt;/li&gt;
&lt;li&gt;물론 저는 무지성으로 구매했습니다. 믿고보는 영한님 강의니까요  &lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실전&amp;nbsp;QueryDSL&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 강의에서 많이 언급하는 QueryDSL + JPA 조합의 다양한 케이스들을 자세하게 설명합니다.&lt;/li&gt;
&lt;li&gt;스프링에서 QueryDSL + JPA 활용 방법과 실제 프로젝트에선 무슨 기능을 사용하면 좋은지 알려줍니다.&lt;/li&gt;
&lt;li&gt;다만 본인이 이미 JPA와 DB SQL에 익숙하다면, 굳이 강의를 보지않아도 구글링-공식문서만으로도 충분하긴 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; &amp;nbsp;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실전&amp;nbsp;스프링&amp;nbsp;데이터&amp;nbsp;JPA&lt;/a&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA에서&amp;nbsp;제공하는&amp;nbsp;기능들을&amp;nbsp;스프링이&amp;nbsp;어떻게&amp;nbsp;추상화시켰는지와&amp;nbsp;사용법에&amp;nbsp;대해&amp;nbsp;설명합니다.&lt;/li&gt;
&lt;li&gt;실무에서&amp;nbsp;자주&amp;nbsp;쓰는&amp;nbsp;기능과,&amp;nbsp;거의&amp;nbsp;쓰지&amp;nbsp;않는&amp;nbsp;기능들을&amp;nbsp;설명해주고&amp;nbsp;그&amp;nbsp;이유를&amp;nbsp;알려줍니다.&lt;/li&gt;
&lt;li&gt;이것도 강의를 먼저 보기보다는, SpringDataJpa를 프로젝트에 사용해보고 강의를 보는게 이해하기 더 쉽습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;실전! 스프링 DB 접근 기술 1편&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC 기초( Connection, DataSource, Query )에 대해 처음부터 학습합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;트랜잭션과 DB 예외처리에 따른 어려움을 학습하고 스프링이 이를 어떻게 해결했는지 하나씩 파헤쳐봅니다.&lt;/li&gt;
&lt;li&gt;Spring Data 접근 기술이나 AOP에 대한 언급은 있으나 이를 자세하게 다루지는 않습니다.&lt;/li&gt;
&lt;li&gt;기본 지식을 채워가는 강의라서 이미 Java로 DB를 많이 다뤄보았거나  익숙하다면 들을 필요는 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;실전! 스프링 DB 접근 기술 2편&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; 실무에서 사용하는 DB 기술들을 종류별로 다 써봅니다 각 기술을 깊이있게 파지는 않고 바로 사용할 수 있을 정도로만 알려줍니다. ( JdbcTemplate, MyBatis, JPA, QueryDsl)&lt;/li&gt;
&lt;li&gt;그래서 이미 써봤다면 앞부분에서 크게 얻어갈건 없긴합니다. 스킵하고 후반부 스프링 트랜잭션 부분만 듣는 것도 추천드립니다.&lt;/li&gt;
&lt;li&gt;후반부에는 스프링 트랜잭션에 대해 깊이있게 다룹니다. ⭐️ @Transactional 의 동작원리에 대해 모른다면 반드시 들어야 할 강의!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt; 스프링 부트 - 핵심 원리와 활용&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;토비님의 스프링 부트 강의와 함께 열심히 듣는 중.. (2024 후기 작성 예정)&lt;/span&gt;&lt;/p&gt;</description>
      <category>  2025/Java 기본지식</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/265</guid>
      <comments>https://jiwondev.tistory.com/265#entry265comment</comments>
      <pubDate>Thu, 15 Feb 2024 02:42:18 +0900</pubDate>
    </item>
    <item>
      <title>Spring DB 기술 파헤치기 #2</title>
      <link>https://jiwondev.tistory.com/296</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqm0tY/btsDkvA1wZA/UtVT6VaP4YZ4lXZUzbPBr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqm0tY/btsDkvA1wZA/UtVT6VaP4YZ4lXZUzbPBr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqm0tY/btsDkvA1wZA/UtVT6VaP4YZ4lXZUzbPBr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdqm0tY%2FbtsDkvA1wZA%2FUtVT6VaP4YZ4lXZUzbPBr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;327&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1704888241518&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;스프링 DB 2편 - 데이터 접근 활용 기술 강의 - 인프런&quot; data-og-description=&quot;백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드 개발자&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LwArc/hyU2jKfFWE/jgFmblKIKHeb5iVeSnxwtK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cldcRN/hyU2ruKbEZ/Kmkd7kXzhvRszKKCH2crx1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/jlMVA/hyU2hlnARw/Klu3xjTNkxJnEe2qvxKEx0/img.png?width=1233&amp;amp;height=772&amp;amp;face=1082_660_1157_742&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LwArc/hyU2jKfFWE/jgFmblKIKHeb5iVeSnxwtK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cldcRN/hyU2ruKbEZ/Kmkd7kXzhvRszKKCH2crx1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/jlMVA/hyU2hlnARw/Klu3xjTNkxJnEe2qvxKEx0/img.png?width=1233&amp;amp;height=772&amp;amp;face=1082_660_1157_742');&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;스프링 DB 2편 - 데이터 접근 활용 기술 강의 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드 개발자&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;
&lt;div&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;  JdbcTemplate &lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1704450448626&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-jdbc'&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;참고로 JPA를 사용한다면 spring-boot-starter-data-jpa 에 jdbc 의존성도 포함되어있어 따로 추가할 필요 없다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UtBPK/btsFEMBy8Ep/vzjtXjU7YlkUpZ36EYAzu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UtBPK/btsFEMBy8Ep/vzjtXjU7YlkUpZ36EYAzu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UtBPK/btsFEMBy8Ep/vzjtXjU7YlkUpZ36EYAzu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUtBPK%2FbtsFEMBy8Ep%2FvzjtXjU7YlkUpZ36EYAzu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;196&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;JdbcTemplate은 DataSource를 이용해 직접 생성하거나 빈으로 등록할 수 있다. 다만 스프링 부트를 사용한다면 그냥 바로 쓰면되는데,&amp;nbsp; JdbcTemplateAutoConfiguration 에 의해 JdbcTemplate, &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;NamedParameterJdbcTemplate&lt;/span&gt; 빈이 없다면 등록하게 구성되어있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;2030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXA5JS/btsFGLVt4md/l7L89iT3LXKNzPMMrBOe10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXA5JS/btsFGLVt4md/l7L89iT3LXKNzPMMrBOe10/img.png&quot; data-alt=&quot;JdbcTemplate은 JdbcOperations 인터페이스의 구현체이다. 즉 수동으로 등록 해둔 빈이 없다면 자동으로 추가한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXA5JS/btsFGLVt4md/l7L89iT3LXKNzPMMrBOe10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXA5JS%2FbtsFGLVt4md%2Fl7L89iT3LXKNzPMMrBOe10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;632&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;2030&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JdbcTemplate은 JdbcOperations 인터페이스의 구현체이다. 즉 수동으로 등록 해둔 빈이 없다면 자동으로 추가한다.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 사용법은 너무나 간단하다. dataSource로 JdbcTemplate를 만들어서 사용하면 된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3230&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7NId8/btsC6VUtcRR/OuVsyhQbEQ9jdDPEG8lQK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7NId8/btsC6VUtcRR/OuVsyhQbEQ9jdDPEG8lQK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7NId8/btsC6VUtcRR/OuVsyhQbEQ9jdDPEG8lQK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7NId8%2FbtsC6VUtcRR%2FOuVsyhQbEQ9jdDPEG8lQK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3230&quot; height=&quot;614&quot; data-origin-width=&quot;3230&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&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;execute() 그냥 sql 자체를 실행하고 싶을 때, 서비스에서 사용할 일은 거의 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704452149341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jdbcTemplate.execute(&quot;create table mytable (id integer, name varchar(100))&quot;);

 jdbcTemplate.update(
         &quot;call SUPPORT.REFRESH_ACTORS_SUMMARY(?)&quot;,
         Long.valueOf(unionId));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;update : 영향받은 row 수를 반환한다. (update, insert, delete)&lt;/li&gt;
&lt;li&gt;insert 를 할 때 자동 생성한 pk 를 얻고싶다면 KeyHolder = new GenereatedKeyHolder()를 update에 같이 넘기면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704452060821&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String sql = &quot;update item set item_name=?, price=?, quantity=? where id=?&quot;;
template.update(sql,
        updateParam.getItemName(),
        updateParam.getPrice(),
        updateParam.getQuantity(),
        itemId);&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;query: 기본 메서드, 잘 사용하지않는다. 단건 조회는 queryForObject() 사용 권장&lt;/li&gt;
&lt;li&gt;queryForXXX: 읽어보면 바로 알 수 있다. 참고로 queryForStream은 타입만 Stream&amp;lt;&amp;gt;일 뿐이다. 병렬처리는 지원하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704452040691&quot; class=&quot;haxe&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
         &quot;select count(*) from t_actor where first_name = ?&quot;, Integer.class,&quot;Joe&quot;);

 String lastName = jdbcTemplate.queryForObject( &quot;select last_name from t_actor where id = ?&quot;, String.class, 1212L);
 
 
 Actor actor = jdbcTemplate.queryForObject(
         &quot;select first_name, last_name from t_actor where id = ?&quot;,
         (resultSet, rowNum) -&amp;gt; {
    Actor newActor = new Actor();
    newActor.setFirstName(resultSet.getString(&quot;first_name&quot;));
    newActor.setLastName(resultSet.getString(&quot;last_name&quot;));
    return newActor;
}, 1212L);

List&amp;lt;Actor&amp;gt; actors = jdbcTemplate.query(
         &quot;select first_name, last_name from t_actor&quot;,
         (resultSet, rowNum) -&amp;gt; {
             Actor actor = new Actor();
             actor.setFirstName(resultSet.getString(&quot;first_name&quot;));
             actor.setLastName(resultSet.getString(&quot;last_name&quot;));
             return actor;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Repository를 구현해보면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1704451259798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JdbcTemplateItemRepository implements ItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepository(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

    @Override
    public Item save(Item item) {
        String sql = &quot;insert into item(item_name, price, quantity) values (?,?,?)&quot;;
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.queryFor
        template.update(connection -&amp;gt; {
            //자동 증가 키
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{&quot;id&quot;});
            ps.setString(1, item.getItemName());
            ps.setInt(2, item.getPrice());
            ps.setInt(3, item.getQuantity());
            return ps;
        }, keyHolder);

        long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        String sql = &quot;update item set item_name=?, price=?, quantity=? where id=?&quot;;
        template.update(sql,
                updateParam.getItemName(),
                updateParam.getPrice(),
                updateParam.getQuantity(),
                itemId);
    }

    @Override
    public Optional&amp;lt;Item&amp;gt; findById(Long id) {
        String sql = &quot;select id, item_name, price, quantity from item where id = ?&quot;;
        try {
            Item item = template.queryForObject(sql, itemRowMapper(), id);
            return Optional.of(item);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    @Override
    public List&amp;lt;Item&amp;gt; findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        String sql = &quot;select id, item_name, price, quantity from item&quot;;
        //동적 쿼리
        if (StringUtils.hasText(itemName) || maxPrice != null) {
            sql += &quot; where&quot;;
        }

        boolean andFlag = false;
        List&amp;lt;Object&amp;gt; param = new ArrayList&amp;lt;&amp;gt;();
        if (StringUtils.hasText(itemName)) {
            sql += &quot; item_name like concat('%',?,'%')&quot;;
            param.add(itemName);
            andFlag = true;
        }

        if (maxPrice != null) {
            if (andFlag) {
                sql += &quot; and&quot;;
            }
            sql += &quot; price &amp;lt;= ?&quot;;
            param.add(maxPrice);
        }

        return template.query(sql, itemRowMapper(), param.toArray());
    }

    private RowMapper&amp;lt;Item&amp;gt; itemRowMapper() {
        return ((rs, rowNum) -&amp;gt; {
            Item item = new Item();
            item.setId(rs.getLong(&quot;id&quot;));
            item.setItemName(rs.getString(&quot;item_name&quot;));
            item.setPrice(rs.getInt(&quot;price&quot;));
            item.setQuantity(rs.getInt(&quot;quantity&quot;));
            return item;
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 미리 만들어둔 편의기능들이 존재한다.&lt;/p&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SimpleJdbcInsert 를 미리 만들어 등록해두면, insert를 메서드 하나로 할 수 있다.&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704451398275&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JdbcTemplateItemRepository implements ItemRepository {

    private final NamedParameterJdbcTemplate template;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
        this.jdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName(&quot;item&quot;)
                .usingGeneratedKeyColumns(&quot;id&quot;)
                .usingColumns(&quot;item_name&quot;, &quot;price&quot;, &quot;quantity&quot;); // usingColumns 는 생략 가능
    }

    @Override
    public Item save(Item item) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        return item;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;NamedParameterJdbcTemplate 를 사용하면 :itemName 으로 바인딩 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;BeanPropertyRowMapper.newInstance( MyClass.class) 같이 편의용 rowMapper를 제공한다.&lt;/span&gt; 필드 camel 변환도 지원!&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/jdbc/simple.html#jdbc-simple-jdbc-call-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SimpleJdbcCall&lt;/a&gt; 같은 프로시저 콜도 있지만, 사용할 일이 없으므로 생략&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704451567411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JdbcTemplateItemRepository implements ItemRepository {

    private final NamedParameterJdbcTemplate template;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
        this.jdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName(&quot;item&quot;) // 테이블 명
                .usingGeneratedKeyColumns(&quot;id&quot;) // pk 명
                // 생략하더라도 DB 메타 읽어서 맞춰줌. 생략 가능하나 특정 칼럼만 사용하고 싶을 때 사용
                .usingColumns(&quot;item_name&quot;, &quot;price&quot;, &quot;quantity&quot;);
    }
    
    @Override
    public Item save(Item item) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        // 이렇게 매우 편하게 쓸 수 있다.
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        return item;
    }
    
    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        String sql = &quot;update item &quot; +
                &quot;set item_name=:itemName, price=:price, quantity=:quantity &quot; +
                &quot;where id=:id&quot;;

        SqlParameterSource param = new MapSqlParameterSource()
                .addValue(&quot;itemName&quot;, updateParam.getItemName())
                .addValue(&quot;price&quot;, updateParam.getPrice())
                .addValue(&quot;quantity&quot;, updateParam.getQuantity())
                .addValue(&quot;id&quot;, itemId); //이 부분이 별도로 필요하다.

        template.update(sql, param);
    }

    
    @Override
    public List&amp;lt;Item&amp;gt; findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        SqlParameterSource param = new BeanPropertySqlParameterSource(cond);

        String sql = &quot;select id, item_name, price, quantity from item&quot;;
        //동적 쿼리
        if (StringUtils.hasText(itemName) || maxPrice != null) {
            sql += &quot; where&quot;;
        }

        boolean andFlag = false;
        if (StringUtils.hasText(itemName)) {
            sql += &quot; item_name like concat('%',:itemName,'%')&quot;;
            andFlag = true;
        }

        if (maxPrice != null) {
            if (andFlag) {
                sql += &quot; and&quot;;
            }
            sql += &quot; price &amp;lt;= :maxPrice&quot;;
        }

        log.info(&quot;sql={}&quot;, sql);
        return template.query(sql, param, itemRowMapper());
    }

    private RowMapper&amp;lt;Item&amp;gt; itemRowMapper() {
        return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;MyBatis&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mybatis.org/mybatis-3/ko/getting-started.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mybatis.org/mybatis-3/ko/getting-started.html&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704452609294&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 필요없으면 공식문서보고 org.mybatis.mybatis 쓰면 된다&lt;/p&gt;
&lt;pre id=&quot;code_1704453231584&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  profiles:
    active: local
  datasource:
    url: jdbc:h2:tcp://localhost/~/test
    username: sa
  logging:
    level:
      org:
        springframework:
          jdbc: debug

mybatis:
  # xml 파일에서 사용할 기본 패키지명, 여기에 적어야 생략 가능하다. ( ; 로 여러 곳을 지정할 수도 있다)
  type-aliases-package: hello.itemservice.domain
  # mapper 클래스의 기본 위치. 이게 없으면 실제 클래스 패키지 위치와 동일하게 resource/../ 에 만들어 주어야한다.
  mapper-locations: classpath:sql/**/*.xml
  # db camel case -&amp;gt; java camelCase 자동 변환
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    hello:
      itemservice:
        repository:
          mybatis: trace&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;Spring MyBatis를 사용하면 구현체를 따로 만들어주지 않아도된다. 그냥 @Mapper를 인터페이스 달아주면 구현체 빈이 생성된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 그냥 myBatis를 사용하면 SqlSessionFactory, SqlSession을 아래와 같이 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1704453388214&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// Open a new SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    // Retrieve data from the database using MyBatis
    List&amp;lt;MyObject&amp;gt; myObjects = sqlSession.selectList(&quot;com.example.MyObjectMapper.selectMyObjects&quot;);
} ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우린 스프링없이 MyBatis 쓸일 없으므로 아래와 같이 @Mapper만 달아주면 끝. 빈으로 스캔된다.&lt;/p&gt;
&lt;pre id=&quot;code_1704452870604&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Mapper
public interface ItemMapper {

    void save(Item item);

    void update(@Param(&quot;id&quot;) Long id, @Param(&quot;updateParam&quot;) ItemUpdateDto updateParam);

    Optional&amp;lt;Item&amp;gt; findById(Long id);

    List&amp;lt;Item&amp;gt; findAll(ItemSearchCond itemSearch);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704452878958&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {

    private final ItemMapper itemMapper;

    @Override
    public Item save(Item item) {
        log.info(&quot;itemMapper class={}&quot;, itemMapper.getClass());
        itemMapper.save(item);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        itemMapper.update(itemId, updateParam);
    }

    @Override
    public Optional&amp;lt;Item&amp;gt; findById(Long id) {
        return itemMapper.findById(id);
    }

    @Override
    public List&amp;lt;Item&amp;gt; findAll(ItemSearchCond cond) {
        return itemMapper.findAll(cond);
    }
}&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;xml path는 실제 클래스와 동일하게 맞춰주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1704452895587&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&amp;lt;mapper namespace=&quot;hello.itemservice.repository.mybatis.ItemMapper&quot;&amp;gt;

    &amp;lt;insert id=&quot;save&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;id&quot;&amp;gt;
        insert into item (item_name, price, quantity)
        values (#{itemName}, #{price}, #{quantity})
    &amp;lt;/insert&amp;gt;

    &amp;lt;update id=&quot;update&quot;&amp;gt;
        update item
        set item_name=#{updateParam.itemName},
            price=#{updateParam.price},
            quantity=#{updateParam.quantity}
        where id = #{id}
    &amp;lt;/update&amp;gt;

    &amp;lt;select id=&quot;findById&quot; resultType=&quot;Item&quot;&amp;gt;
        select id, item_name, price, quantity
        from item
        where id = #{id}
    &amp;lt;/select&amp;gt;

    &amp;lt;select id=&quot;findAll&quot; resultType=&quot;Item&quot;&amp;gt;
        select id, item_name, price, quantity
        from item
        &amp;lt;where&amp;gt;
            &amp;lt;if test=&quot;itemName != null and itemName != ''&quot;&amp;gt;
                and item_name like concat('%', #{itemName}, '%')
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;maxPrice != null&quot;&amp;gt;
                and price &amp;amp;lt;= #{maxPrice}
            &amp;lt;/if&amp;gt;
        &amp;lt;/where&amp;gt;
    &amp;lt;/select&amp;gt;

&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sqlSessionTemplate 직접 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 추상화해서 사용하기 싫은 분들을 위해 sqlSessionTemplate 을 스프링에서 제공해준다.  조금 더 직관적으로 사용할 수 있다.대신 MyBatis의 구조에 대해 조금 이해하고 사용해야한다.&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;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE4jbr/btsC8VfoNwx/KdepZkhIZHWFQIpwW1o6Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE4jbr/btsC8VfoNwx/KdepZkhIZHWFQIpwW1o6Sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE4jbr/btsC8VfoNwx/KdepZkhIZHWFQIpwW1o6Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE4jbr%2FbtsC8VfoNwx%2FKdepZkhIZHWFQIpwW1o6Sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;774&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qRbA7/btsC7ncbKVl/alqH87WABhIpwVXmmOi7I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qRbA7/btsC7ncbKVl/alqH87WABhIpwVXmmOi7I0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qRbA7/btsC7ncbKVl/alqH87WABhIpwVXmmOi7I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqRbA7%2FbtsC7ncbKVl%2FalqH87WABhIpwVXmmOi7I0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1950&quot; height=&quot;1800&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 제공하는 sqlSessionFactoryBean을 등록해서 쓰면 된다. sqlSessionTemplate을 추가해놓자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 추가하지 않더라도 MybatisAutoConfiguration 에 의해서 아래 빈들은 자동으로 다 등록된다. 수동으로 설정할 때만 사용&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JTj85/btsC4GjcNsj/KqINB5ugMBpuazOjT8Ky4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JTj85/btsC4GjcNsj/KqINB5ugMBpuazOjT8Ky4K/img.png&quot; data-alt=&quot;스프링 마이베티스 의존성을 불러오면, 스프링 부트가 MybatisAutoConfiguration 를 빈으로 등록한다. 해당 클래스에 다 있음&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JTj85/btsC4GjcNsj/KqINB5ugMBpuazOjT8Ky4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJTj85%2FbtsC4GjcNsj%2FKqINB5ugMBpuazOjT8Ky4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;328&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스프링 마이베티스 의존성을 불러오면, 스프링 부트가 MybatisAutoConfiguration 를 빈으로 등록한다. 해당 클래스에 다 있음&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;pre id=&quot;code_1704453944515&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class DatabaseConfiguration {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        var sqlSessionFactory = new SqlSessionFactoryBean();

        // yml 설정을 코드로 직접 추가
        sqlSessionFactory.setDataSource(dataSource);

        var configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.setConfiguration(configuration);
        sqlSessionFactory.setTypeHandlersPackage(&quot;com.your.package&quot;);
        
        // mapper-locations: classpath:sql/**/*.xml 등록
        var resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactory.setMapperLocations(resolver.getResources(&quot;classpath:mapper/xml/*.xml&quot;));

        return sqlSessionFactory.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}&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;설정에보면 typeHandlers 스캔 패키지 경로를 지정해줄 수 있는데, 이는 Enum Converter처럼 특정 DB 타입을 매핑할 때 사용된다&lt;/p&gt;
&lt;pre id=&quot;code_1704455337543&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// RoleType 컨버터
@MappedTypes(Role.class)
public final class RoleTypeHandler implements TypeHandler&amp;lt;Role&amp;gt; {

    @Override
    public void setParameter(PreparedStatement ps, int i,
        Role parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter.name());
    }

    @Override
    public Role getResult(ResultSet rs, String columnName) throws SQLException {
        return Role.findRole(rs.getString(columnName));
    }

    @Override
    public Role getResult(ResultSet rs, int columnIndex) throws SQLException {
        return Role.findRole(rs.getString(columnIndex));
    }

    @Override
    public Role getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return Role.findRole(cs.getString(columnIndex));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 코드&lt;/p&gt;
&lt;pre id=&quot;code_1704453725777&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class MybatisAccountRepository implements AccountRepository, AccountReader {

    private static final String SAVE_FQCN = &quot;com.yam.app.account.domain.AccountRepository.save&quot;;
    private static final String UPDATE_FQCN = &quot;com.yam.app.account.domain.AccountRepository.update&quot;;

    private final SqlSessionTemplate template;

    public MybatisAccountRepository(SqlSessionTemplate template) {
        this.template = template;
    }

    @Override
    public boolean existsByEmail(String email) {
        return template.getMapper(AccountReader.class).existsByEmail(email);
    }

    @Override
    public void update(Account entity) {
        int result = template.update(UPDATE_FQCN, entity);
        if (result != 1) {
            throw new IllegalStateException(String.format(
                &quot;Unintentionally, more records were updated than expected. : %s&quot;, entity));
        }
    }

    @Override
    public void save(Account entity) {
        int result = template.insert(SAVE_FQCN, entity);
        if (result != 1) {
            throw new IllegalStateException(String.format(
                &quot;Unintentionally, more records were saved than expected. : %s&quot;, entity));
        }
    }

    @Override
    public Optional&amp;lt;Account&amp;gt; findByEmail(String email) {
        return template.getMapper(AccountReader.class).findByEmail(email);
    }

    @Override
    public MemberAccount findByEmailAndMemberId(String email,
        Long memberId) {
        return template.getMapper(AccountReader.class).findByEmailAndMemberId(email, memberId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;JPA, Hibernate&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://jiwondev.tistory.com/category/%F0%9F%8C%B1Backend/JDBC%20%26%20JPA?page=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jiwondev.tistory.com/category/%F0%9F%8C%B1Backend/JDBC%20%26%20JPA?page=1&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jiwondev.tistory.com/252&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;figure id=&quot;og_1704876526908&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 성능 개선팁&quot; data-og-description=&quot;인프런 김영한님 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의 스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니&quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/252&quot; data-og-url=&quot;https://jiwondev.tistory.com/252&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cFNuHU/hyU2jKbHWc/CueK6yfO94SK0HO7DdxVak/img.png?width=365&amp;amp;height=275&amp;amp;face=0_0_365_275,https://scrap.kakaocdn.net/dn/fGHXS/hyU2r2wLN0/Nr1ekuFKpAWj0Df89Rn2kk/img.png?width=365&amp;amp;height=275&amp;amp;face=0_0_365_275,https://scrap.kakaocdn.net/dn/c9NHLi/hyU2q3z4cI/0Os4p0XCqfry7WaaXIleF0/img.png?width=849&amp;amp;height=380&amp;amp;face=0_0_849_380&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/252&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cFNuHU/hyU2jKbHWc/CueK6yfO94SK0HO7DdxVak/img.png?width=365&amp;amp;height=275&amp;amp;face=0_0_365_275,https://scrap.kakaocdn.net/dn/fGHXS/hyU2r2wLN0/Nr1ekuFKpAWj0Df89Rn2kk/img.png?width=365&amp;amp;height=275&amp;amp;face=0_0_365_275,https://scrap.kakaocdn.net/dn/c9NHLi/hyU2q3z4cI/0Os4p0XCqfry7WaaXIleF0/img.png?width=849&amp;amp;height=380&amp;amp;face=0_0_849_380');&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 성능 개선팁&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인프런 김영한님 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의 스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&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;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스프링 데이터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;JPA&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/253&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jiwondev.tistory.com/253&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704452397372&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 Data JPA&quot; data-og-description=&quot;기억안날 때 쉽게 찾으려고 한 글에 다 적었습니다. * 스크롤 압박 주의 * 스프링 설정을 쉽게 해주는 Spring Boot 프로젝트가 있듯이 스프링에서 데이터를 쉽게 사용하게 해주는 Spring Data 프로젝트&quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/253&quot; data-og-url=&quot;https://jiwondev.tistory.com/253&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bLa8tI/hyUXWPultE/syd2JHGKJ5byQo7HpCviK1/img.png?width=709&amp;amp;height=623&amp;amp;face=0_0_709_623,https://scrap.kakaocdn.net/dn/68xyu/hyUXNdWd0w/x9jfUSZhVYoh2ZFKUFxPO1/img.png?width=709&amp;amp;height=623&amp;amp;face=0_0_709_623,https://scrap.kakaocdn.net/dn/bujFFA/hyUXViHAJ8/sOob4oMeqZq6BOtwGdCrI0/img.png?width=918&amp;amp;height=609&amp;amp;face=0_0_918_609&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/253&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/253&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bLa8tI/hyUXWPultE/syd2JHGKJ5byQo7HpCviK1/img.png?width=709&amp;amp;height=623&amp;amp;face=0_0_709_623,https://scrap.kakaocdn.net/dn/68xyu/hyUXNdWd0w/x9jfUSZhVYoh2ZFKUFxPO1/img.png?width=709&amp;amp;height=623&amp;amp;face=0_0_709_623,https://scrap.kakaocdn.net/dn/bujFFA/hyUXViHAJ8/sOob4oMeqZq6BOtwGdCrI0/img.png?width=918&amp;amp;height=609&amp;amp;face=0_0_918_609');&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 Data JPA&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기억안날 때 쉽게 찾으려고 한 글에 다 적었습니다. * 스크롤 압박 주의 * 스프링 설정을 쉽게 해주는 Spring Boot 프로젝트가 있듯이 스프링에서 데이터를 쉽게 사용하게 해주는 Spring Data 프로젝트&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나만 추가하자면, JPA의 의존성을 인터페이스로 완전히 감출 수 있다. 우선 아래와 같이 인터페이스를 정의한다. &lt;span style=&quot;color: #0593d3;&quot;&gt;이렇게 분리할 경우, 인터페이스 분리 원칙에 따라 Repository -&amp;gt;&amp;nbsp; { Reader , Writer } 로 인터페이스를 분리해서 쓰는 방법도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704879215828&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface SampleRepository {
    Optional&amp;lt;SampleEntity&amp;gt; findById(Long id);

    SampleEntity save(SampleEntity entity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 1️⃣ JPA 구현체만 사용한다면 아래와 같이 인터페이스를 추가하면 깔끔하게 사용할 수 있다. &lt;span style=&quot;color: #0593d3;&quot;&gt;참고로 QueryDsl등을 쓴다면 interface CustomJpaRepository 를 만들고, SampleJpaRepository가 상속하게 하면 동일하게 쓸 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704879239800&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 참고로 @Repository를 생략해도 @EnableJpaRepositories 에 의해 JpaRepository 타입이 스캔되어 빈 등록된다.
@Repository 
public interface SampleJpaRepository extends SampleRepository, JpaRepository&amp;lt;SampleEntity, Long&amp;gt; {

    @Override
    Optional&amp;lt;SampleEntity&amp;gt; findById(Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ 여러 DB 구현체를 사용한다면 컴포지션을 활용해서, 아예 별도의 RepositoryImpl 클래스를 만들어서 이용해도 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1704879668223&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface SampleJpaRepository extends JpaRepository&amp;lt;SampleEntity, Long&amp;gt; {

}

@RequiredArgsConstructor
public class SampleRepositoryImpl implements SampleRepository {

    private final SampleJpaRepository jpaRepository;
    private final SampleCustomRepository customRepository;

    @Override
    public Optional&amp;lt;SampleEntity&amp;gt; findById(Long id) {
        return customRepository.findById(id);
    }

    @Override
    public SampleEntity save(SampleEntity entity) {
        return jpaRepository.save(entity);
    }
}&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;하지만 결국 모든건 트레이드 오프라는 걸 명심하자.  우리는 왜 DIP 원칙을 지키고, 추상화시켜서 코드를 분리하는걸까? 결국 &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;현실적으로 DB 의존성이 바뀌는 경우가 얼마나 될까? 아니, 애초에 인터페이스로 분리한다고 해도 DB를 쉽게 바꿀 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 것은 트레이드 오프이다.&lt;/b&gt; 서비스에 따라 Repository 자체가 너무나 복잡하다면 위 예제처럼 분리하는게 훨씬 나을 수 있다. 반대로 그 정도로 큰 서비스가 아니거나 당장 만드는게 급하다면 JpaRepository를 바로 사용하는 것이 정답일 수도 있다.&lt;/p&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; &amp;nbsp;Querydsl&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0070d1; text-align: start;&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/255&quot;&gt;https://jiwondev.tistory.com/255&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704882531197&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의 영속성컨텍스트 (EntityManager)&quot; data-og-description=&quot;  JPA (하이버네이트)의 영속성 컨텍스트 JPA에서 영속성 컨텍스트는 DB에서 가져온 엔티티를 저장하는 첫번째 메모리 캐시 저장소입니다.이를 EntityManager 객체의 API로 관리할 수 있습니다. 영속&quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/255&quot; data-og-url=&quot;https://jiwondev.tistory.com/255&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lInK6/hyU2jco77r/yDGvqpDcFeAyum1qYm0xkk/img.png?width=659&amp;amp;height=244&amp;amp;face=0_0_659_244,https://scrap.kakaocdn.net/dn/bzj75J/hyU2evmSlr/mW0baGdQ9kl2HVrC6E0Gv1/img.png?width=659&amp;amp;height=244&amp;amp;face=0_0_659_244,https://scrap.kakaocdn.net/dn/bxIabx/hyU2gtag8R/DZw8k1ujppI4brkKZeYLz0/img.png?width=659&amp;amp;height=244&amp;amp;face=0_0_659_244&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/255&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/255&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lInK6/hyU2jco77r/yDGvqpDcFeAyum1qYm0xkk/img.png?width=659&amp;amp;height=244&amp;amp;face=0_0_659_244,https://scrap.kakaocdn.net/dn/bzj75J/hyU2evmSlr/mW0baGdQ9kl2HVrC6E0Gv1/img.png?width=659&amp;amp;height=244&amp;amp;face=0_0_659_244,https://scrap.kakaocdn.net/dn/bxIabx/hyU2gtag8R/DZw8k1ujppI4brkKZeYLz0/img.png?width=659&amp;amp;height=244&amp;amp;face=0_0_659_244');&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의 영속성컨텍스트 (EntityManager)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  JPA (하이버네이트)의 영속성 컨텍스트 JPA에서 영속성 컨텍스트는 DB에서 가져온 엔티티를 저장하는 첫번째 메모리 캐시 저장소입니다.이를 EntityManager 객체의 API로 관리할 수 있습니다. 영속&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1962&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N5UoK/btsDhNiliuN/YYkW4LxkCtaVk247IJVrIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N5UoK/btsDhNiliuN/YYkW4LxkCtaVk247IJVrIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N5UoK/btsDhNiliuN/YYkW4LxkCtaVk247IJVrIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN5UoK%2FbtsDhNiliuN%2FYYkW4LxkCtaVk247IJVrIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1962&quot; height=&quot;718&quot; data-origin-width=&quot;1962&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 QuerydslRepositorySupport라고 따로 지원해주는게 있긴한데 사용해보면 알겠지만 살짝 불편하다.&lt;/p&gt;
&lt;pre id=&quot;code_1704881441110&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속 (MemberRepositoryCustom)
public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt;, MemberRepositoryCustom {
    List&amp;lt;Member&amp;gt; findByUsername(String username);
}

public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
    public MemberRepositoryImpl() {
        super(Member.class);
    }

    //  QuerydslRepositorySupport 사용
    //  from절이 먼저 시작합니다.
    private Page&amp;lt;MemberTeamDto&amp;gt; getMemberTeamDtoQueryResults(MemberSearchCondition condition, Pageable pageable) {
        JPQLQuery&amp;lt;MemberTeamDto&amp;gt; jpaQuery = from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .select(new QMemberTeamDto(
                        member.id.as(&quot;memberId&quot;),
                        member.username,
                        member.age,
                        team.id.as(&quot;teamId&quot;),
                        team.name.as(&quot;teamName&quot;)));

        JPQLQuery&amp;lt;MemberTeamDto&amp;gt; query = getQuerydsl().applyPagination(pageable, jpaQuery);// offset, limit 지원

        query.fetch();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차라리 QueryDsl에서 제공하는 JpaQueryFactory (* EntityManager와 1:1 관계)를 직접 사용하는게 훨씬 깔끔하고 편하다. 참고로 Spring Jpa에서는 `Impl` 이라는 prefix를 붙이면 자동으로 빈 스캔이 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1704881613432&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt;, MemberRepositoryCustom {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704881664294&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. Jpa가 아닌 다른걸로 확장하고 싶은 메서드는 여기에다가 따로 정의해둔다.
public interface MemberRepositoryCustom {
    List&amp;lt;Member&amp;gt; findMemberCustom();
}

//2. 참고로 'Custom' 은 꼭 안붙여도 된다. 뒤에 'Impl' 이름을 기준으로 같은 패키지안의 클래스를 스캔한다.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
 
    private final EntityManager em;
    public MemberRepositoryImpl(EntityManager em) { this.em = em; }
 
    @Override
    public List&amp;lt;Member&amp;gt; findMemberCustom() {
        return em.createQuery(&quot;select m from Member m&quot;)
            .getResultList();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 `Impl` 접두사 설정 또한 @EnableJpaRepository에서 커스텀할 수 있다. 이는 스프링 부트가 Jpa 의존성이있으면 설정해줌.&lt;/p&gt;
&lt;pre id=&quot;code_1704881798171&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableJpaRepositories(basePackages = &quot;study.datajpa.repository&quot;, // 물론 스캔 패키지도 변경가능
                            repositoryImplementationPostfix = &quot;Impl&quot;) // 여기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이름 규칙을 사용하지 않고 그냥  JpaRepository, QueryDslRepostiory로 따로 만들어서 사용해도 된다. &lt;span style=&quot;color: #0593d3;&quot;&gt;만드는건 더 귀찮아지지만 service 에서 하나만 쓰고싶다면 ItemRepository 자바 인터페이스를 만들고, ItemRepositoryImpl 에서 실제 구현체를 빈으로 받아와서 아래 코드처럼 쓰면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704882364218&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public class ItemQueryRepository{
       private final JPAQueryFactory query;
}

@Service
public class ItemService {
    private final ItemRepositoryV2 itemRepositoryV2;
    private final ItemQueryRepositoryV2 itemQueryRepositoryV2;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/254&quot;&gt;https://jiwondev.tistory.com/254&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704880267338&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;QueryDSL + JPA&quot; data-og-description=&quot;참고로 아래와 같이 설정하면 하이버네이트가 어떻게 동작하는지 확인할 수 있다. spring: datasource: url: jdbc:h2:tcp://localhost/~/test username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create p&quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/254&quot; data-og-url=&quot;https://jiwondev.tistory.com/254&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pikkm/hyU2oShPuG/GaS6ZuuBgGrvYogRAzCX21/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/xeNJf/hyU2hS9QXt/H3bU5hHgzcqCIypkdkUdlK/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/jyqCE/hyU2e9XP8t/sRVHf6KQSmKqN5wY8YTsHK/img.png?width=822&amp;amp;height=642&amp;amp;face=0_0_822_642&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/254&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/254&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pikkm/hyU2oShPuG/GaS6ZuuBgGrvYogRAzCX21/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/xeNJf/hyU2hS9QXt/H3bU5hHgzcqCIypkdkUdlK/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/jyqCE/hyU2e9XP8t/sRVHf6KQSmKqN5wY8YTsHK/img.png?width=822&amp;amp;height=642&amp;amp;face=0_0_822_642');&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;QueryDSL + JPA&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;참고로 아래와 같이 설정하면 하이버네이트가 어떻게 동작하는지 확인할 수 있다. spring: datasource: url: jdbc:h2:tcp://localhost/~/test username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create p&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  트랜잭션 전파옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 @Transactional로 손쉽게 트랜잭션을 걸 수 있게 만들어놨지만, 사실 실제 트랜잭션 동작과 100% 일치하는건 아닙니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 @Transactional 로 건 것을 논리 트랜잭션이라고 표현합니다.&lt;/li&gt;
&lt;li&gt;JDBC를 통해 실제로 걸리는 부분을 물리 트랜잭션이라고 표현합니다.&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;1962&quot; data-origin-height=&quot;1900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t2xQ3/btsDhiXfMNh/f4n0fstSOtHIB8mk1QmLk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t2xQ3/btsDhiXfMNh/f4n0fstSOtHIB8mk1QmLk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t2xQ3/btsDhiXfMNh/f4n0fstSOtHIB8mk1QmLk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft2xQ3%2FbtsDhiXfMNh%2Ff4n0fstSOtHIB8mk1QmLk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;645&quot; data-origin-width=&quot;1962&quot; data-origin-height=&quot;1900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 개념을 나눠서 JDBC에는 없는 트랜잭션 전파라는 재밌는 기능을 스프링이 구현해놓았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704885844374&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기본값은 REQUIRED, 없으면 걸고 있으면 기존 트랜잭션을 같이 쓴다.
 @Transactional(propagation = Propagation.REQUIRES_NEW)&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;1280&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oHU70/btsDj3ktOJL/XpfbCyxJ9m4RZZKHDIr2T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oHU70/btsDj3ktOJL/XpfbCyxJ9m4RZZKHDIr2T0/img.png&quot; data-alt=&quot;사실 전파 옵션을 변경할 일은 잘 없긴하다. 구조 자체를 변경하는게 더 나으니까&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oHU70/btsDj3ktOJL/XpfbCyxJ9m4RZZKHDIr2T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoHU70%2FbtsDj3ktOJL%2FXpfbCyxJ9m4RZZKHDIr2T0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;673&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;673&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;  기본값 REQUIRED에서 고려해야하는 문제들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberService, MemberRepository, LogRepository에 각각 트랜잭션이 걸려있다고 가정해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 논리적으로 @Transaction을 전체로 걸었지만, 실제로는 아래와 같이 하나의 JDBC 물리 트랜잭션으로 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LgRKH/btsDllEGSYr/ifL0om5chVmlKCKX0TFO4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LgRKH/btsDllEGSYr/ifL0om5chVmlKCKX0TFO4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LgRKH/btsDllEGSYr/ifL0om5chVmlKCKX0TFO4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLgRKH%2FbtsDllEGSYr%2FifL0om5chVmlKCKX0TFO4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;354&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1️⃣ (외부 롤백) 당연히 최상위 클래스에서 에러가 발생하면, 전체 트랜잭션이 롤백됩니다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1vtgN/btsDiaxGmnO/bQPwE11xNKthCuOVIv7Xm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1vtgN/btsDiaxGmnO/bQPwE11xNKthCuOVIv7Xm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1vtgN/btsDiaxGmnO/bQPwE11xNKthCuOVIv7Xm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1vtgN%2FbtsDiaxGmnO%2FbQPwE11xNKthCuOVIv7Xm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;264&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2️⃣ (내부 롤백) 일부만 성공하고 내부에서 Runtime 에러가 발생하더라도 전체가 롤백됩니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 try-catch로 에러를 처리하더라도 UnexpectedRollbackException 가 발생하며 전체 트랜잭션이 롤백됩니다. &lt;span style=&quot;color: #0593d3;&quot;&gt;물론 @Transactional(rollbackOnly=false) 설정이나 (rollbackFor=MyClass.class) 로 바꿀 순 있지만 권장하지 않습니다.&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;1596&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L6kbd/btsDkDMtEf7/bBTmdqSQAwgB1B1AkGwhzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L6kbd/btsDkDMtEf7/bBTmdqSQAwgB1B1AkGwhzK/img.png&quot; data-alt=&quot;트랜잭션동기화 매니저가 rollback을 기록합니다. 만약 트랜잭션을 추상화해서 엄청 크게 잡았다면 바로 찾기 어렵습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L6kbd/btsDkDMtEf7/bBTmdqSQAwgB1B1AkGwhzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL6kbd%2FbtsDkDMtEf7%2FbBTmdqSQAwgB1B1AkGwhzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;363&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;트랜잭션동기화 매니저가 rollback을 기록합니다. 만약 트랜잭션을 추상화해서 엄청 크게 잡았다면 바로 찾기 어렵습니다.&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;976&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n0hQH/btsDit4Jl4X/8UUL9MHrxnSl1bXFsKVSj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n0hQH/btsDit4Jl4X/8UUL9MHrxnSl1bXFsKVSj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n0hQH/btsDit4Jl4X/8UUL9MHrxnSl1bXFsKVSj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn0hQH%2FbtsDit4Jl4X%2F8UUL9MHrxnSl1bXFsKVSj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;411&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;976&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3️⃣ AOP 특성상 this를 통한 내부 호출은 트랜잭션이 걸리지 않습니다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM은 class, method 등의 정보는 딱 한번만 static 영역에 저장합니다.&lt;/li&gt;
&lt;li&gt;이후 각 클래스의 인스턴스가 만들어질 때 힙메모리를 할당합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;즉 인스턴스는 class, method를 가지고 있지 않고 각각의 힙 메모리를 가지고 있습니다. 이 메모리는 this 로 참조할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP는 상속을 활용한 프록시 객체를 통해 걸립니다. this 로 참조해버리면 프록시 객체를 사용하지 않기 때문에 트랜잭션이 걸리지 않습니다.&lt;/p&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;3️⃣  스프링 @Transactional 은 public 메서드만 지원합니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스에 붙이는건 그 클래스가 가진 모든 public 메서드에만 적용됩니다. &lt;span style=&quot;color: #0593d3;&quot;&gt;물론 protected와 default 접근제어자는 상속이 가능한데 스프링이 정책적으로 막아뒀습니다. 애초에 거기에 트랜잭션이 걸어야하는 상황 자체도 없고, 그런식의 동작도 이상하니까요.&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;다행히 메서드에 직접 걸면 IntelliJ가 바로 경고를 띄워주기에 실수할 일이 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;1736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LTXL7/btsDiQ6K2kM/20BuW9oCCKW4uM6cLipLd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LTXL7/btsDiQ6K2kM/20BuW9oCCKW4uM6cLipLd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LTXL7/btsDiQ6K2kM/20BuW9oCCKW4uM6cLipLd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLTXL7%2FbtsDiQ6K2kM%2F20BuW9oCCKW4uM6cLipLd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;574&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;1736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Class에 @Transactional을 붙이는 경우 따로 알려주지 않으니 주의하도록 합시다. 트랜잭션은&amp;nbsp;&lt;b&gt;public Method 에만 걸립니다.&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;994&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLOc8D/btsDfW7Gngq/7Ufze4lPbpqJSvFbJGNWh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLOc8D/btsDfW7Gngq/7Ufze4lPbpqJSvFbJGNWh1/img.png&quot; data-alt=&quot;에러가 안뜬다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLOc8D/btsDfW7Gngq/7Ufze4lPbpqJSvFbJGNWh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLOc8D%2FbtsDfW7Gngq%2F7Ufze4lPbpqJSvFbJGNWh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;166&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;372&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4️⃣ 전파옵션으로 해결할 수 있지만.. 그러지말고 계층을 분리합시다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Required_New 등으로 트랜잭션을 분리할 수 있습니다만.. 동작을 예측하기도 어렵고 오히려 더 큰 버그를 만들어 낼 수 있습니다.&lt;span style=&quot;color: #0593d3;&quot;&gt; 원래는 에러터지고 말았다면, 트랜잭션 전파옵션을 통해 똑같은 회원이 3번씩 저장되는 이상한 버그를 만들 수 도 있죠&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;그냥 애초부터 트랜잭션을 분리하면 됩니다. 최상단에 @Transacational을 시작하지 마세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q4ec9/btsDj3LzF2H/iSplRtLJq34Yfw6hSMsJl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q4ec9/btsDj3LzF2H/iSplRtLJq34Yfw6hSMsJl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q4ec9/btsDj3LzF2H/iSplRtLJq34Yfw6hSMsJl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq4ec9%2FbtsDj3LzF2H%2FiSplRtLJq34Yfw6hSMsJl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1556&quot; height=&quot;866&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기로 동작해서 위 그림처럼 컨트롤 할 수 없다면 @TransactionEventListener 등을 활용하는 방법도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704887884170&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Service(
    private val eventPublisher: ApplicationEventPublisher
) {
    @Transactional
    fun signUp(dto: MemberSignUpRequest) {
        val member = createMember(dto.toEntity())
        eventPublisher.publishEvent(MemberSignedUpEvent(member)) // 이벤트 발송
    }
}

class EventHandler{
    // 아래의 메서드는 eventPublisher 코드에 있는 트랜잭션이 성공해야 동작합니다. 
    @TransactionalEventListener
    fun memberSignedUpEventListener(event: MemberSignedUpEvent) {
        emailSenderService.sendSignUpEmail(event.member)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 범위가 엄청 복잡해서 상세하게 걸고싶다면, Spring @Transactional을 사용하지말고 TransactionTemplate을 직접 쓰면 됩니다. 사용법만 다를 뿐 똑같이 동작합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704888094549&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MyService {
    private final TransactionTemplate transactionTemplate;
    
    /** ⭐️ Java8 람다로 정의된 메서드를 쓰면 매우 깔끔합니다. */
    void myTransactionalLambda() {
        transactionTemplate.executeWithoutResult(status -&amp;gt; { /*..트랜잭션 로직..*/ });
        
        var result = transactionTemplate.execute(status -&amp;gt; { /*..트랜잭션 로직..*/ return null; });
    }
    
    /** 익명클래스로 사용할 땐 아래와 같습니다 */
    MyResultDto myTransactionalMethod() {
        return transactionTemplate.execute(new TransactionCallback&amp;lt;MyResultDto&amp;gt;() {
            @Override
            public MyResultDto doInTransaction(TransactionStatus status) {
                // 여기에 트랜잭션을 적용할 비즈니스 로직을 작성합니다.
                // 필요한 경우 status.setRollbackOnly(); 호출로 롤백을 지시할 수 있습니다.
                MyResultDto result = new MyResultDto();
                return result;
            }
        });
    }
 
    void myTransactionalMethod() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                // 여기에 트랜잭션을 적용할 비즈니스 로직을 작성합니다.
                // 필요한 경우 status.setRollbackOnly(); 호출로 롤백을 지시할 수 있습니다.
            }
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  2025/JDBC &amp;amp; JPA</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/296</guid>
      <comments>https://jiwondev.tistory.com/296#entry296comment</comments>
      <pubDate>Thu, 11 Jan 2024 00:02:05 +0900</pubDate>
    </item>
    <item>
      <title>스프링 배치를 사용하면서 겪은 것들</title>
      <link>https://jiwondev.tistory.com/295</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPpEoX/btsC7o3zLVb/ZSxnmSW7W6Weyo5XlNOOMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPpEoX/btsC7o3zLVb/ZSxnmSW7W6Weyo5XlNOOMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPpEoX/btsC7o3zLVb/ZSxnmSW7W6Weyo5XlNOOMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPpEoX%2FbtsC7o3zLVb%2FZSxnmSW7W6Weyo5XlNOOMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;371&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&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;&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;스프링 배치는 Job 단위로 실행됩니다. 하나의 Job은 여러 Step으로 나눌 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3242&quot; data-origin-height=&quot;1266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewDGa5/btsC35X6Ylx/kv1E3ofpKSSypIzKSHz6PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewDGa5/btsC35X6Ylx/kv1E3ofpKSSypIzKSHz6PK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewDGa5/btsC35X6Ylx/kv1E3ofpKSSypIzKSHz6PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FewDGa5%2FbtsC35X6Ylx%2Fkv1E3ofpKSSypIzKSHz6PK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3242&quot; height=&quot;1266&quot; data-origin-width=&quot;3242&quot; data-origin-height=&quot;1266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면서 Spring Batch 내부의 Job Repository를 통해 계속해서 Job과 Step의 정보를 아래의 순서대로 메타 테이블에 저장한다. 참고로 메타 정보는 배치 수행과 관련된 수치(시작/종료 시간, 상태, 횟수)와 Job, Step에서 공유해서 쓰는 컨텍스트등을 포함 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #323232; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BATCH_JOB_INSTANCE :&amp;nbsp; JOB 인스턴스 정보 - Job 이름, Job 고유 키&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION :&amp;nbsp; JOB 실행정보 - job_instance_id, 상태, 시작시간, 종료시간, job 에러정보&amp;nbsp;&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION_PARAM :&amp;nbsp; 사용된 Job Parameter 정보&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION_CONTEXT :&amp;nbsp; 사용된 Job Execution Context 객체 &lt;span style=&quot;color: #323232; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(직렬화되어 DB에도 저장)&lt;/span&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;BATCH_STEP_EXECUTION :&amp;nbsp; STEP 실행정보 - job_execution_id,  데이터 read, commit, filter, skip count, step 에러정보&lt;/li&gt;
&lt;li&gt;BATCH_STEP_EXECUTIOIN_CONTEXT : 사용된 Step Execution Context 객체 (직렬화되어 DB에도 저장)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l9NyW/btsC2zrCP4e/RzWQSeywM9PeVEBUS1T2Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l9NyW/btsC2zrCP4e/RzWQSeywM9PeVEBUS1T2Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l9NyW/btsC2zrCP4e/RzWQSeywM9PeVEBUS1T2Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl9NyW%2FbtsC2zrCP4e%2FRzWQSeywM9PeVEBUS1T2Dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1467&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1467&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; 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;960&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9bv7A/btsC381AQy6/5eefT5XMO6pJ5pdMM9Dbn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9bv7A/btsC381AQy6/5eefT5XMO6pJ5pdMM9Dbn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9bv7A/btsC381AQy6/5eefT5XMO6pJ5pdMM9Dbn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9bv7A%2FbtsC381AQy6%2F5eefT5XMO6pJ5pdMM9Dbn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;552&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 트랜잭션 부분을 확대해보면 아래와 같다. 참고로 Reader에서 DB를 사용하지 않더라도 spring batch 메타 데이터를 갱신하면서 바로 DB 트랜잭션이 시작됨을 유의하자.&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;2062&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eRgw2B/btsC34EWFE6/yYrc6rrhSibPjVWHyDG2a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eRgw2B/btsC34EWFE6/yYrc6rrhSibPjVWHyDG2a0/img.png&quot; data-alt=&quot;Chunk 단위로 트랜잭션이 반복된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eRgw2B/btsC34EWFE6/yYrc6rrhSibPjVWHyDG2a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeRgw2B%2FbtsC34EWFE6%2FyYrc6rrhSibPjVWHyDG2a0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2062&quot; height=&quot;1026&quot; data-origin-width=&quot;2062&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Chunk 단위로 트랜잭션이 반복된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이는 ChunkOrientedTasklet 를 디버깅으로 추적해보면 코드로도 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bayR1K/btsAxagVB1L/Wj9KIiiBkMk7ZL7eCLas50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bayR1K/btsAxagVB1L/Wj9KIiiBkMk7ZL7eCLas50/img.png&quot; data-alt=&quot;정확히는 TaskletStep 에서 작업할 때 마다 StepExecution이 업데이트 되고 이는 청크 단위로 JobRepository에 의해 DB에 저장됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bayR1K/btsAxagVB1L/Wj9KIiiBkMk7ZL7eCLas50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbayR1K%2FbtsAxagVB1L%2FWj9KIiiBkMk7ZL7eCLas50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1896&quot; height=&quot;1178&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정확히는 TaskletStep 에서 작업할 때 마다 StepExecution이 업데이트 되고 이는 청크 단위로 JobRepository에 의해 DB에 저장됩니다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;  스프링 배치는 왜 이렇게 트랜잭션을 길게 잡을까요?&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;SpringBatch는 청크가 실행 될 때 마다 StepExecution 객체 업데이트, 청크가 끝나는 시점에 DB에 저장한다. 이는 배치 작업의 추적과 오류 발생시 재시작을 위해 사용된다.&lt;/li&gt;
&lt;li&gt;복잡한 배치 작업에 &lt;a href=&quot;https://github.com/spring-projects/spring-batch/issues/1916&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TransactionCallback을 사용 시 데드락이 걸리는 이슈&lt;/a&gt; 가 있어서 청크단위로 트랜잭션을 잡은거기도 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1️⃣ &lt;span style=&quot;text-align: start;&quot;&gt;TransactionSystemException (Transaction Session Timeout)&lt;/span&gt;&lt;/span&gt;&lt;/h2&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&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704627574663&quot; class=&quot;css&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;org.springframework.transaction.TransactionSystemException: Could not roll back JPA transaction; nested exception is org.hibernate.TransactionException: Unable to rollback against JDBC Connection
	at org.springframework.orm.jpa.JpaTransactionManager.doRollback(JpaTransactionManager.java:593)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:835)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.rollback(AbstractPlatformTransactionManager.java:809)
	at org.springframework.transaction.support.TransactionTemplate.rollbackOnException(TransactionTemplate.java:168)
	at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:144)
	...&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; 데이터를 API로 받아와서 전처리하는 배치였는데 Reader와 Processor에서는 DB 를 사용하지 않는 코드였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Writer 또한 단순 전처리완료된 데이터를 Save하는 단순한 쿼리라 더더욱 이해가 안되는 상황이었다.&lt;/p&gt;
&lt;pre id=&quot;code_1704627631788&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;/** 예시입니다. **/
@Bean
fun JiwonCollectDataJob(
    jiwonStep: Step,
): Job {
    return jobBuilderFactory.get(JIWON_JOB_NAME)
        .start(jiwonStep)
        .incrementer(RunIdIncrementer())
        .listener(JobResultListener())
        .build()
}

@Bean
fun JiwonStep(
    jiwonItemReader: JiwonItemReader,
    jiwonItemProcessor: ItemProcessor&amp;lt;List&amp;lt;CollectData&amp;gt;, List&amp;lt;MyEntity&amp;gt;&amp;gt;,
    jiwonItemWriter: ItemWriter&amp;lt;List&amp;lt;MyEntity&amp;gt;&amp;gt;,
): Step {
    // Reader(웹사이트 API) - Processor(복잡한 데이터 전처리) - Writer(단순 DB 저장)
    return stepBuilderFactory[JIWON_STEP_NAME]
        .chunk&amp;lt;List&amp;lt;ParseData&amp;gt;, List&amp;lt;Entity&amp;gt;&amp;gt;(chunkSize)
        .reader(jiwonItemReader)
        .processor(jiwonItemProcessor)
        .writer(jiwonItemWriter)
        .transactionManager(mainDbTransactionManager)
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;  왜  Transaction Timeout이 발생했는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면 Hikari Connection 객체는 살아있는데  db session timeout 을 초과했기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704630187930&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 관련된 로그 (Spring, db, HikariCP)
org.springframework.transaction.TransactionSystemException: Could not roll back JPA transaction; nested exception is org.hibernate.TransactionException: Unable to rollback against JDBC Connection
FATAL: terminating connection due to idle-in-transaction timeout
HikariCP-Writer - Connection org.postgresql.jdbc.PgConnection@2d0ea3a9 marked as broken because of SQLSTATE(08006), ErrorCode(0) // 08006은 커넥션 풀이 사용하고있는 커넥션이 중단되었거나 문제임을 나타냄.&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;1251&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXCCBw/btsC2Cu9SHB/CPm3ItX2hIwmHEPCoMeKr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXCCBw/btsC2Cu9SHB/CPm3ItX2hIwmHEPCoMeKr0/img.png&quot; data-alt=&quot;PostgreSql, MySql 에서 각각 timeout 시간을 위와 같이 확인할 수 있다. //  aws rds를 사용중이라면 파라미터 그룹에서도 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXCCBw/btsC2Cu9SHB/CPm3ItX2hIwmHEPCoMeKr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXCCBw%2FbtsC2Cu9SHB%2FCPm3ItX2hIwmHEPCoMeKr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1251&quot; height=&quot;420&quot; data-origin-width=&quot;1251&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PostgreSql, MySql 에서 각각 timeout 시간을 위와 같이 확인할 수 있다. //  aws rds를 사용중이라면 파라미터 그룹에서도 확인할 수 있다.&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;Reader에서 DB를 사용하지 않았다고하더라도, 청크가 시작되는 시점에 step_execution을 업데이트하면서 db를 사용한다. 그리고 Writer가 동작하기전까지 DB를 사용하지않는데, 그 시간이 위 timeout (60초)를 초과했을 때만 간헐적으로 발생했던 것이었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;chunk 단위 작업 시작 시, 트랜잭션 생성으로 인해 HikariCP 에서 커넥션을 획득합니다.&lt;/li&gt;
&lt;li&gt;커넥션을 이미 받았더라도 쿼리를 실행하지 않으면 DB에 요청을 보내지 않습니다.&lt;/li&gt;
&lt;li&gt;즉 DB 입장에서는 connection이 유후 상태입니다.&amp;nbsp;idle_in_transaction_session_timeout (60초)가 지나면 커넥션이 끊깁니다.&lt;/li&gt;
&lt;li&gt;신나게 Reader와 Processor가 동작합니다. 처리할 데이터가 많아서 60초를 지나버립니다. 이 때 HikariCP 커넥션은 살아있지만, DB 커넥션은 이미 끊긴상태입니다.&lt;/li&gt;
&lt;li&gt;이후 writer에서 작업을 하든, spring batch에서 stepExecution을 저장하려고 하든 커넥션이 끊겨버렸기 때문에 아무것도 안됩니다.&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;3394&quot; data-origin-height=&quot;1478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8lY3b/btsC52mn1yh/VT9JiDbx3zZXDS3sCfKuKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8lY3b/btsC52mn1yh/VT9JiDbx3zZXDS3sCfKuKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8lY3b/btsC52mn1yh/VT9JiDbx3zZXDS3sCfKuKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8lY3b%2FbtsC52mn1yh%2FVT9JiDbx3zZXDS3sCfKuKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3394&quot; height=&quot;1478&quot; data-origin-width=&quot;3394&quot; data-origin-height=&quot;1478&quot;/&gt;&lt;/span&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  어떻게 해결하지? - ResourcelessTransactionManager&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순하게 idle-transaction-session-timeout을 무작정 늘리는 방법도 있겠지만 이는 DB에 부담을 줄 수 있다.&lt;/li&gt;
&lt;li&gt;session이 끊기지 않도록 계속해서 쿼리를 날려주는 방법도 있겠지만 불필요한 쿼리를 생성해야하고 세션 자체의 누수 위험이 있다.&lt;br /&gt;-&amp;gt; &lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://pkgonan.github.io/2018/04/HikariCP-test-while-idle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;왜 HikariCP는 커넥션(세 션) 갱신 기능을 절대 만들어주지 않을까?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 이런 경우를 위해 Spring Batch에선 &lt;b&gt;ResourcelessTransactionManager&lt;/b&gt; 라는걸 따로 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704630825926&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Step step() throws Exception {
    return stepBuilderFactory.get(&quot;myStep&quot;)
            .&amp;lt;ParseData, Item&amp;gt;chunk(chunkSize)
            .reader(reader())
            .processor(processor())
            .writer(writer())
            .transactionManager(new ResourcelessTransactionManager()) // No Transaction
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게말하면 SpringBatch에서 필요없는 트랜잭션을 걸지않고&lt;s&gt; StepExceution과 같은 메타 정보를 테이블에 저장하지 않는다.&lt;/s&gt; &lt;br /&gt;(수정) JobRepository로 메타정보는 저장한다. 청크단위로 트랜잭션만 걸지 않을 뿐이다. ResourceLessTxManager 코드를 열어보면 이해가 쉬울텐데, 그냥 동작하는 척하는 간단한 코드로 구성되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;1290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dACuKj/btsGNDQmO5j/0ETKakMkspkxPz3qKy7jJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dACuKj/btsGNDQmO5j/0ETKakMkspkxPz3qKy7jJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dACuKj/btsGNDQmO5j/0ETKakMkspkxPz3qKy7jJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdACuKj%2FbtsGNDQmO5j%2F0ETKakMkspkxPz3qKy7jJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;372&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;1290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 동작에는 변함이 없는데 단지 [청크단위 트랜잭션]이 없을 뿐이다.  배치가 아닌 일반코드에서 Repository를 사용하는 것과 동일&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4924&quot; data-origin-height=&quot;2128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh2tjC/btsGMTfk3Gw/n4nHNKFMLl20cXaABcgwH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh2tjC/btsGMTfk3Gw/n4nHNKFMLl20cXaABcgwH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh2tjC/btsGMTfk3Gw/n4nHNKFMLl20cXaABcgwH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh2tjC%2FbtsGMTfk3Gw%2Fn4nHNKFMLl20cXaABcgwH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4924&quot; height=&quot;2128&quot; data-origin-width=&quot;4924&quot; data-origin-height=&quot;2128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;물론 만능은 아니고 아래와 같은 상황에서 유용하게 사용할 수 있다. &lt;span style=&quot;color: #0593d3;&quot;&gt;당연한거지만 Reader에서 내 코드로 커넥션을 직접 열고 안쓰다가 Writer에서 재사용하면 처음과 동일하게 timeout이 발생할 수 있다.&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;Reader 에서만 DB를 사용하고 Writer에서 사용하지 않는 경우 &lt;span style=&quot;color: #000000;&quot;&gt;(세션이 닫혀도 직접 쿼리를 날리지 않으니 아무 문제 없다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Writer 에서만 DB를 사용하고 Reader에서 사용하지 않는 경우 (세션 자체가 writer 시점에 열린다.)&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;1280&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbiZS7/btsC54RZZhD/Uv7PyIGDPcmqyeFoHk3b4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbiZS7/btsC54RZZhD/Uv7PyIGDPcmqyeFoHk3b4k/img.png&quot; data-alt=&quot;https://kwonnam.pe.kr/wiki/springframework/batch&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbiZS7/btsC54RZZhD/Uv7PyIGDPcmqyeFoHk3b4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbiZS7%2FbtsC54RZZhD%2FUv7PyIGDPcmqyeFoHk3b4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;426&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://kwonnam.pe.kr/wiki/springframework/batch&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;2️⃣ 가능하면 JPA 영속성 컨텍스트를 사용하지 말자.&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치작업을 하면서 별 생각없이 이미 만들어진 Domain 코드의 JpaRepository를 이용하거나 삭제 작업등에는 JpaCursorItemReader등을 활용하곤 했 었다. 처음에는 아무 문제 없었지만 작업량이 커지니  사용자가 많이 없는데도 DB CPU 사용률이 높다는 알람을 받게 된다.&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;3000&quot; data-origin-height=&quot;1233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXNhtr/btsDdDFjdR3/duOG64mhopwMWPrL92yHR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXNhtr/btsDdDFjdR3/duOG64mhopwMWPrL92yHR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXNhtr/btsDdDFjdR3/duOG64mhopwMWPrL92yHR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXNhtr%2FbtsDdDFjdR3%2FduOG64mhopwMWPrL92yHR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;274&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1233&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치작업의 특성상 ChunkSize 만큼 I/O가 발생하게된다. JPA는 더티체킹으로 업데이트를 진행하기 때문에 단건으로조회해서 Writer가 진행된다. 즉 갯수만큼 select 요청을 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;이를 해결하는건 간단했었다. JdbcReader를 사용하거나 Projections 을 이용해서 JPA 영속성 컨텍스트를 거치지않고 바로 업데이트하도록 코드를 변경하니 쉽게 해결되었다. 또 JPA를 사용하지 않으니 쿼리로 update .. where in (ids) 와 같이 최적화 할 수 있는 부분이 있는건 덤&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;update 작업 자체가 많아서 변경이 힘들다면 DB I/O 작업을 묶어서 처리하는 jdbc executeBatch를 활용할 수도 있다.&lt;/li&gt;
&lt;li&gt;그 외 &lt;a href=&quot;https://tech.kakaopay.com/post/ifkakao2022-batch-performance-read/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tech.kakaopay.com/post/ifkakao2022-batch-performance-read/&lt;/a&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;1556&quot; data-origin-height=&quot;1396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx8xku/btsC4eneLs7/QxcOQ23TL5nqTmK5tTT540/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx8xku/btsC4eneLs7/QxcOQ23TL5nqTmK5tTT540/img.png&quot; data-alt=&quot;https://tech.kakaopay.com/post/spring-batch-performance/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx8xku/btsC4eneLs7/QxcOQ23TL5nqTmK5tTT540/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx8xku%2FbtsC4eneLs7%2FQxcOQ23TL5nqTmK5tTT540%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;598&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;1396&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://tech.kakaopay.com/post/spring-batch-performance/&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;3️⃣ Domain 로직이나 변하는 값(time, option)은 배치에서 분리하자.&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 매달 1일에 실행하도록 만드는 배치라서 LocalDateTime.now() 같이 코드에 박아서 사용할 수 있는데, 그러지 않는걸 권장한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버시간이나 실행환경에 따라 값이 달라질 수 있다.&lt;/li&gt;
&lt;li&gt;당장은 매달 1일만 실행할 것 같지만, 상황에 따라 (혹은 배치가 실패해서) 다른 날짜에 실행할 수도 있다.&lt;/li&gt;
&lt;li&gt;옵션값이 변경되 면 배치를 새로 배포해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 바로 구할 수 있더라도 위와 같은걸 고려해서 JobParameters 으로 받아서 사용하자.&lt;/p&gt;
&lt;pre id=&quot;code_1704632759959&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Value(&quot;#{jobParameters[batchDate]}&quot;) batchDate: String,
@Value(&quot;#{jobParameters[apiKey]}&quot;) apiKey: String&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;같은 맥락으로 굳이 필요하지 않다면 Batch에서 가능하면 Domain에 구현된 코드를 사용하지 않고 별도의 모듈로 분리하기를 권장한다.&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;1362&quot; data-origin-height=&quot;1230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0Hbum/btsC4CVLGCk/lvQkxIlZ1gdViBw5KTHM7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0Hbum/btsC4CVLGCk/lvQkxIlZ1gdViBw5KTHM7k/img.png&quot; data-alt=&quot;https://techblog.woowahan.com/2637/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0Hbum/btsC4CVLGCk/lvQkxIlZ1gdViBw5KTHM7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0Hbum%2FbtsC4CVLGCk%2FlvQkxIlZ1gdViBw5KTHM7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;601&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://techblog.woowahan.com/2637/&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;4️⃣ Update, Delete 배치작업시 데이터가 누락되지 않게 주의하자&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 간단한건데 Update, Delete 배치 작업의 경우 페이징이 깨진다는 걸 절대 잊지말자. 별거 아닌데 막상 돌려보면 잘 돌아가는 것 같이 보이기 때문에 모르고 있으면 정말 찾기 힘들다  &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pagingReader로 10개를 읽어옴&lt;/li&gt;
&lt;li&gt;read 10개 -&amp;gt; writer 10 개&lt;/li&gt;
&lt;li&gt;writer 10개를 했기 때문에 중간에 몇 개를 건너뛰고 다음 페이지가 반환됨&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;제일 편한 해결 방법은 CursorReader로 읽어들이거나, 그게 어렵다면 아래와 같이 pageSize=0으로 고정해서 읽으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 pageSize 를 고정한 경우 모든 아이템이 완료되지 않는다면 배치가 끊나지 않는 다는걸 명심하자.&lt;/p&gt;
&lt;pre id=&quot;code_1704633489703&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@StepScope
public ItemReader&amp;lt;Order&amp;gt; itemReader() {
    MyBatisPagingItemReader&amp;lt;Order&amp;gt; itemReader = new MyBatisPagingItemReader&amp;lt;Order&amp;gt;() {
        @Override
        public int getPage() {
            return 0;
        }
    };
    itemReader.setQueryId(&quot;query-name&quot;);
    itemReader.setPageSize(PAGE_SIZE);
    itemReader.setSqlSessionFactory(sqlSessionFactory);
    return itemReader;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704633456225&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Kotlin이면 이렇게.
val reader: JpaPagingItemReader&amp;lt;Group&amp;gt; = object : JpaPagingItemReader&amp;lt;Group&amp;gt;() {
    override fun getPage(): Int {
        return 0
    }
}.apply {
    pageSize = batchPageSize
    setName(&quot;reader&quot;)
    setEntityManagerFactory(entityManagerFactory)
    setQueryString( &quot;SELECT * FROM Group&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;a href=&quot;https://jojoldu.tistory.com/526&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jojoldu.tistory.com/526&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kwonnam.pe.kr/wiki/springframework/batch&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kwonnam.pe.kr/wiki/springframework/batch&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pkgonan.github.io/2018/04/HikariCP-test-while-idle&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pkgonan.github.io/2018/04/HikariCP-test-while-idle&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kakaocommerce.tistory.com/45&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kakaocommerce.tistory.com/45&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=164123&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=164123&lt;/a&gt;&lt;/p&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;figure id=&quot;og_1704633603213&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;스프링 배치 강의 - 인프런&quot; data-og-description=&quot;초급에서 중~고급에 이르기까지 스프링 배치의 기본 개념부터 API 사용법과 내부 아키텍처 구조를 심도있게 다룹니다. 그리고 스프링 배치 각 기능의 흐름과 원리를 학습하게 되고 이를 바탕으&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pHtyq/hyUXLgszgg/VIiHrUkdApDfi6VQpcOt71/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/IzKFx/hyU2rm4bbU/Rw4Hc9xrR7JXN9ne3L9Fik/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/b5cqNJ/hyU2lmQE91/kJpDkPt77qkxV2cgML3AnK/img.png?width=2116&amp;amp;height=1100&amp;amp;face=0_0_2116_1100&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&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pHtyq/hyUXLgszgg/VIiHrUkdApDfi6VQpcOt71/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/IzKFx/hyU2rm4bbU/Rw4Hc9xrR7JXN9ne3L9Fik/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/b5cqNJ/hyU2lmQE91/kJpDkPt77qkxV2cgML3AnK/img.png?width=2116&amp;amp;height=1100&amp;amp;face=0_0_2116_1100');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;스프링 배치 강의 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;초급에서 중~고급에 이르기까지 스프링 배치의 기본 개념부터 API 사용법과 내부 아키텍처 구조를 심도있게 다룹니다. 그리고 스프링 배치 각 기능의 흐름과 원리를 학습하게 되고 이를 바탕으&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;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;</description>
      <category>  2025/Spring Batch</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/295</guid>
      <comments>https://jiwondev.tistory.com/295#entry295comment</comments>
      <pubDate>Wed, 10 Jan 2024 21:42:28 +0900</pubDate>
    </item>
    <item>
      <title>DB 인덱스에서는 왜 HashTable을 사용하지 않을까?</title>
      <link>https://jiwondev.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스의 인덱스는 보통 밸런스 트리(B-Tree)를 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;# 밸런스 트리(Balanced Tree)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 트리의 노드가 한 방향으로 쏠리지 않도록, 노드 삽입 및 삭제 시 특정 규칙에 맞게 재 정렬되어 왼쪽과 오른쪽 자식 양쪽 수의 밸런스를 유지하는 트리이다. 항상 양쪽 자식의 밸런스를 유지하므로, 트리의 평균시간복잡도 O(logN)을 항상 보장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUJ6Yo/btrat1CR2rW/kJDU0UTYGkRb2BwjOJVi71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUJ6Yo/btrat1CR2rW/kJDU0UTYGkRb2BwjOJVi71/img.png&quot; data-alt=&quot;오른쪽과 같은 상황이 벌어지지 않게, 밸런스 트리를 이용한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUJ6Yo/btrat1CR2rW/kJDU0UTYGkRb2BwjOJVi71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUJ6Yo%2Fbtrat1CR2rW%2FkJDU0UTYGkRb2BwjOJVi71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;262&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;372&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;- 다만 재정렬되는 작업으로 인해 노드 삽입 및 삭제 시 일반적인 트리보다 성능이 떨어지게 된다. 그러므로 밸런스 트리는 삽입/삭제의 성능을 희생하고 탐색에 대한 성능을 높였다고 볼 수 있다. 밸런스 트리는 대표적으로 RedBlack-Tree, B-Tree가 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;# 왜 탐색이 가장 빠른 해시테이블을 인덱스에 사용하지 않나요?&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa;&quot;&gt;해시테이블이 탐색에 있어 상수시간 O(1)을 보장하는 건 맞다. 하지만 여기에는 함정이 있는데, '&lt;/span&gt;&lt;u&gt;단 하나의 데이터를 탐색할 때&lt;/u&gt;&lt;span style=&quot;background-color: #fafafa;&quot;&gt;' 해당되는 이야기이다. 데이버테이스에서 검색은 등호(=) 뿐 아니라 부등호(&amp;lt;, &amp;gt;)도 사용 할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa;&quot;&gt;즉, 모든 값이 정렬되어 있지 않은 해시테이블은 특정 기준보다 크거나 작은 값을 찾을 수 없다. 즉 DB의 인덱스로는 사용할 수 없는 자료구조인 것이다.&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;1200&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/shiuP/btraBSR6tCE/0l4AgEtLkHKkkwcYkw8DE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/shiuP/btraBSR6tCE/0l4AgEtLkHKkkwcYkw8DE0/img.png&quot; data-alt=&quot;해시테이블을 이용한 인덱스 예제&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/shiuP/btraBSR6tCE/0l4AgEtLkHKkkwcYkw8DE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FshiuP%2FbtraBSR6tCE%2F0l4AgEtLkHKkkwcYkw8DE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;405&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;876&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;# 왜 RedBlack-Tree가 아닌 B-Tree를 사용하는거죠?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- RedBlack-Tree 와 B-Tree는 각각 다른 특징을 가진다. 먼저 RedBlack-Tree를 살펴보자&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ RedBlack-Tree&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레드-블랙 트리는 이진 트리의 특수한 형태로 삽입, 삭제, 검색에서 항상 일정한 실행시간을 보장하는 밸런스 트리의 일종이다. 레드-블랙 트리는 2가지 노드(레드,블랙)를 다음과 같은 규칙으로 이진트리를 구성한다&lt;br /&gt;&lt;br /&gt;1. 루트와 모든 마지막 노드(null leaf, NIL)는 블랙이다.&lt;br /&gt;2. 레드 노드의 양쪽 자식은 언제나 블랙이다. (= 레드는 연달아 나올수 없고, 항상 블랙이 부모이다)&lt;br /&gt;3. 어떤 노드에서 시작하여 그에 속한 하위 리프 노드에 도달하는 모든 경로에는 같은 개수의 블랙노드를 가지고 있다. (* 리프 노드 자신은 제외)&lt;br /&gt;=&amp;gt; 이러한 규칙을 이용하여 [루트~가장 먼 곳]의 거리가 [루트~ 가장 가까운 곳]의 두 배보다 작게 이진트리를 구성한다. 즉 시간복잡도가 트리의 깊이에 따라 결정되므로 트리의 삽입, 삭제, 검색 성능을 일정하게 유지시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2u0FB/btrasvxmYWH/GFg4qytHbmgcMlhpTn6YVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2u0FB/btrasvxmYWH/GFg4qytHbmgcMlhpTn6YVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2u0FB/btrasvxmYWH/GFg4qytHbmgcMlhpTn6YVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2u0FB%2FbtrasvxmYWH%2FGFg4qytHbmgcMlhpTn6YVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;245&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ B-Tree&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree는 노드하나에 여러 데이터를 저장할 수 있다. 각 노드 내부는 항상 정렬된 상태이며 현재 노드안에 있는 데이터의 범위를 이용하여 자식노드를 만든다. 즉 각 노드는 최대 (n+1)개의 자식노드를 가지게 된다. 보통 B-Tree는 O(log N)의 참조 시간을 갖기 위해 한 노드의 데이터 개수를 3~5개정도로 제한하여 사용한다.&lt;br /&gt;&lt;br /&gt;아래 그림을 통해 알아보자. 루트 노드안에 3개의 값이 있다. 각 값의 범위 (0~100, 100~155, 155~266, 266~)를 이용하여 최대 4개 (3+1)의 자식노드를 생성한다. 이렇게 구성하면 트리를 탐색 할 때 마치 배열처럼 순서대로 접근할수도 있고 특정 값을 탐색할 때 밸런스 트리처럼 O(log N)의 시간복잡도를 가지게 된다.&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;231&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDLSvr/btrasTRTsZV/Bim8K9iPSSTKNdQpPMtUQ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDLSvr/btrasTRTsZV/Bim8K9iPSSTKNdQpPMtUQ1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDLSvr/btrasTRTsZV/Bim8K9iPSSTKNdQpPMtUQ1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDLSvr%2FbtrasTRTsZV%2FBim8K9iPSSTKNdQpPMtUQ1%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;666&quot; height=&quot;219&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;231&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ RedBalck-Tree 와 B-Tree의 큰 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 논리적인 시간복잡도로 생각하면 둘 다 O(log N)의 성능을 낸다.&lt;br /&gt;하지만 하드웨어적인 측면에서 봤을 때 성능은 크게 차이난다. 그 이유는 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;참조 포인터의 사용&lt;/u&gt;.&lt;br /&gt;&lt;br /&gt;참조 포인터를 사용하는 Linked-List와 인덱스를 사용하는 Array의 차이를 떠올려보자.&lt;br /&gt;배열 같이 상수 인덱스로 접근하는 경우, 정말로 정렬된 실제 메모리에서 단순 덧셈으로 한번에 접근하는거라 탐색 속도가 굉장히 빠르다. 하지만 참조 포인터의 경우 계속해서 해당 메모리 주소를 타고 들어가서 원하는 값을 가져오기에 탐색 속도가 느리다.&lt;br /&gt;&lt;br /&gt;RedBlack-Tree의 경우 각각의 노드를 참조 포인터만 사용해서 접근하지만, B-Tree의 경우 같은 노드의 값은 배열처럼 상수 인덱스 덧셈으로 빠르게 데이터에 접근 할 수 있다. 즉 특정 값을 탐색하거나 부등호(&amp;lt;, &amp;gt;) 등의 연산에서 순서대로 값을 가져올 때 훨씬 좋은 효율을 낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;@ 그럼 참조 포인터를 사용 안하는 Array 쓰면 더 좋은거 아니에요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree가 아니라 아예 배열을 사용해서 전부 상수 인덱스로 접근한다면 값을 가져올 때 훨씬 빠르게 가져올 수 있는건 맞다. 심지어 정렬된 배열을 사용하면 부등호 연산도 B-Tree보다 더 빠르게 처리 할 수있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 배열은 '접근'만 좋을 뿐이지 데이터의 저장, 삭제가 일어나는 순간 끔찍한 성능을 보여주게 된다. 그러니 모든 면으로 적절한 밸런스를 가지고 있는 B-Tree가 데이터베이스에는 가장 적합한 것이다.실제로 B-Tree 를 설명할 때 '데이터베이스나 파일 시스템에 널리 활용되는 밸런스 잡힌 트리구조'라고 하기도 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;# B-Tree 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 노드안의 배열은 항상 정렬된 상태로 있어 크고 작은 부등호 연산에 문제가 없다.&lt;br /&gt;2. 배열의 인덱스 접근을 적절하게 섞어서 사용하기에 방대한 데이터 탐색에도 빠른 메모리 접근을 지원한다.&lt;br /&gt;3. 밸런스 트리라서 저장, 수정, 삭제에도 O(logN)의 복잡도를 항상 보장한다.&lt;/p&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 인덱스는 배열과 밸런스트리의 장점을 적절히 섞어서 사용하는 B-Tree가 제일 적합하다.&lt;/p&gt;</description>
      <category>  2025/CS 기본지식</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/112</guid>
      <comments>https://jiwondev.tistory.com/112#entry112comment</comments>
      <pubDate>Wed, 10 Jan 2024 21:42:13 +0900</pubDate>
    </item>
    <item>
      <title>Spring DB 기술 파헤치기 #1</title>
      <link>https://jiwondev.tistory.com/294</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/603Ql/btsCMymc0Fy/paFhlEsYocK5tMar1lvvrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/603Ql/btsCMymc0Fy/paFhlEsYocK5tMar1lvvrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/603Ql/btsCMymc0Fy/paFhlEsYocK5tMar1lvvrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F603Ql%2FbtsCMymc0Fy%2FpaFhlEsYocK5tMar1lvvrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;327&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1704008159575&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;스프링 DB 1편 - 데이터 접근 핵심 원리 강의 - 인프런&quot; data-og-description=&quot;백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bnt2Zr/hyUXJOI8B2/A1fsS1IknqPDwFfkBobDy0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/JYIYr/hyUXUQfEbB/jbk9OP8U0jHukk3llLRaw0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bnWZIk/hyUTxoR7pO/34J3kQpcNiSYyrJf3KkWqk/img.png?width=3000&amp;amp;height=2064&amp;amp;face=0_0_3000_2064&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bnt2Zr/hyUXJOI8B2/A1fsS1IknqPDwFfkBobDy0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/JYIYr/hyUXUQfEbB/jbk9OP8U0jHukk3llLRaw0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bnWZIk/hyUTxoR7pO/34J3kQpcNiSYyrJf3KkWqk/img.png?width=3000&amp;amp;height=2064&amp;amp;face=0_0_3000_2064');&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;스프링 DB 1편 - 데이터 접근 핵심 원리 강의 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔&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;&lt;br /&gt;Java의 데이터 접근기술은 1997년 JDBC (JDK 1.1)에 시작되어 20년의 역사를 거치며 발전해왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; Oracle, MySql, PostgreSql 같은 벤더를 제외하고 Java의 DB 접근기술만 공부한다해도 그 규모가 너무 커져 왜 이렇게 사용하는지, 내부에서는 어떻게 동작하는지를 이해하기는 쉽지 않습니다.&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;866&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/da269R/btsCAhKgHAA/34eCuKgiMU2gjeVFLbM1KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/da269R/btsCAhKgHAA/34eCuKgiMU2gjeVFLbM1KK/img.png&quot; data-alt=&quot; 게다가 기타 도구들 Hibernate Envers, Hypersistence(vladmihalcea), flyway ... 단순 사용법만 찾아도 끝이 안보이죠&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/da269R/btsCAhKgHAA/34eCuKgiMU2gjeVFLbM1KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fda269R%2FbtsCAhKgHAA%2F34eCuKgiMU2gjeVFLbM1KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;457&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 게다가 기타 도구들 Hibernate Envers, Hypersistence(vladmihalcea), flyway ... 단순 사용법만 찾아도 끝이 안보이죠&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  자바 코드로 DB를 사용하는 방법&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wnmqP/btsCUzDIgKm/0lzBojDPlg7ASW3UcSi2tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wnmqP/btsCUzDIgKm/0lzBojDPlg7ASW3UcSi2tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wnmqP/btsCUzDIgKm/0lzBojDPlg7ASW3UcSi2tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwnmqP%2FbtsCUzDIgKm%2F0lzBojDPlg7ASW3UcSi2tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;493&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1704007866535&quot; class=&quot;reasonml&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    // JDBC 4.0 부터는 META-INF/services/java.sql.Driver를 읽어 DriverManager를 미리 생성해줍니다.
    // 필요하다면 Class.forName(&quot;com.my.db.jdbc.Driver&quot;) 와 같이 직접 DriverManager에 추가할 수도 있습니다.
    connection = DriverManager.getConnection(jdbcUrl, username, password)
    println(&quot;Database connection established.&quot;)
 
    // Statement 생성
    statement = connection.createStatement()
 
    // SQL 쿼리 실행
    val sql = &quot;SELECT * FROM your_table&quot;
    resultSet = statement.executeQuery(sql)
 
    // 결과 처리
    while (resultSet.next()) {
        // 첫 번째 컬럼 값을 String으로 읽기
        val firstColumnValue = resultSet.getString(1)
        println(firstColumnValue)
    }
} catch (e: Exception) {
    e.printStackTrace()
} finally {
    // DB 커넥션을 닫아줘야 합니다. Java7 부터는 try-resource 문 (kotlin 에선 use)를 사용하면 편합니다.
    resultSet?.close()
    statement?.close()
    connection?.close()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;  JDBC&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 데이터베이스를 사용하는 방법은 JDBC(Java  DataBase Connectivity) API 단 하나 뿐이다. 이는 자바에서 제공해주는 DB 연결 인터페이스이고&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실제 구현체는 공식 DB 벤더사 (Oracle, MS..)에서 제공합니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #0593d3;&quot;&gt;만약&amp;nbsp;나만의&amp;nbsp;Driver를&amp;nbsp;구현하고&amp;nbsp;싶다면&amp;nbsp;동일하게&amp;nbsp;JDBC&amp;nbsp;Driver&amp;nbsp;구현체를&amp;nbsp;만들어서&amp;nbsp;등록하면&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;JDBC는 크게 2가지 인터페이스로 이루어져있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwUZ3z/btsCX613N0X/njCHmZRtgh4v8R8ohS3Hjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwUZ3z/btsCX613N0X/njCHmZRtgh4v8R8ohS3Hjk/img.png&quot; data-alt=&quot;DataAccess는 개발자가 직접 만든 코드를 의미한다 (DAO, Data Access Object, Repository 등으로도 불린다.)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwUZ3z/btsCX613N0X/njCHmZRtgh4v8R8ohS3Hjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwUZ3z%2FbtsCX613N0X%2FnjCHmZRtgh4v8R8ohS3Hjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;131&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DataAccess는 개발자가 직접 만든 코드를 의미한다 (DAO, Data Access Object, Repository 등으로도 불린다.)&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;JDBC Driver: 실제 DB와 네트워크 통신을 담당한다.&lt;/li&gt;
&lt;li&gt;DataSource : 각종 설정 및 커넥션 풀을 관리한다.&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;** 권장하는 방법은 아니지만, 단순히 한번 연결 하는거라면 DriverManger  유틸을 직접 꺼내 간단히 커넥션을 얻는 방법도 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;실제 구현체는 자바의 공식 DB 벤더사(MS, Oracle...)가 만들어서 제공하며  Java1.4 이후 버전 자바 설치시 META-INF/services/java.sql.Driver 경로에 포함되어있다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;참고로 그 이전엔 Driver 구현체를 직접 로드해서 사용했는데, 컴파일 시점에 모든 DB Driver 클래스를 읽는건 불필요하므로 Class.forName(&quot;oracle.jdbc.driver.OracleDriver&quot;) 메서드를 이용해 런타임에 필요한 DB 구현체만 포함 되도록 사용했었다.&lt;/blockquote&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;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dR2X1E/btsCTykOIhm/xUJwLvu6fWwkuKIjp7S8ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dR2X1E/btsCTykOIhm/xUJwLvu6fWwkuKIjp7S8ok/img.png&quot; data-alt=&quot;DriverManager에 등록된 JDBC Driver는 드라이버 객체 생성시점에 static {..} 으로 클래스를 읽어와서 등록된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dR2X1E/btsCTykOIhm/xUJwLvu6fWwkuKIjp7S8ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdR2X1E%2FbtsCTykOIhm%2FxUJwLvu6fWwkuKIjp7S8ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;334&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DriverManager에 등록된 JDBC Driver는 드라이버 객체 생성시점에 static {..} 으로 클래스를 읽어와서 등록된다.&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;h2 data-ke-size=&quot;size26&quot;&gt;  Java DataSource&amp;nbsp; 와 DB Connection Pool (DBCP)&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 JDBC Connection 객체를 이용해 DB와 통신을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB는 커넥션에 클라이언트용 세션을 따로 만들어 요청을 받고 원하는 응답을 제공합니다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;참고로 자바 어플리케이션이 아닌 데이터베이스에서 사용되는 Connection과 Session의 의미는 아래와 같다.&lt;br /&gt;&lt;br /&gt;&amp;lt; 커넥션이 연결되면 세션이 만들어지게 되고, 세션에 작업을 요청하면 프로세스가 실행 된다. &amp;gt;&lt;br /&gt;- 커넥션(Connection)은 클라이언트 프로세스와 데이터베이스 인스턴스간의 물리적인 TCP/IP 통신 경로를 의미한다.&lt;br /&gt;- 세션(Sessions)은 논리적인 개념으로 한 유저의 로그인 상태를 의미한다. 보통 커넥션과 1:1 으로 대응된다.&lt;br /&gt;- 프로세스(Process)는 실제 서버에 생성되는 운영체제 프로세스를 의미한다.&lt;br /&gt;&lt;br /&gt;다만  해당 DB가 자체적인 Connection Pool이나 Shared Server Mode 같은 걸 제공해준다면 한 커넥션에 여러개의 세션이 붙을 수도 있다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcehIl/btsCzKza5Xz/UuHVbJBrQsRMparqdUdmj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcehIl/btsCzKza5Xz/UuHVbJBrQsRMparqdUdmj1/img.png&quot; data-alt=&quot; DataSource는 DB 커넥션 풀링과 서버 설정을 담당하고, JDBC Driver는 실제 벤더(Oracle, MySql..) 와 통신을 담당합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcehIl/btsCzKza5Xz/UuHVbJBrQsRMparqdUdmj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcehIl%2FbtsCzKza5Xz%2FUuHVbJBrQsRMparqdUdmj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;226&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; DataSource는 DB 커넥션 풀링과 서버 설정을 담당하고, JDBC Driver는 실제 벤더(Oracle, MySql..) 와 통신을 담당합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;참고로 자바 어플리케이션이 아닌 데이터베이스에서 사용되는 Connection과 Session의 의미는 아래와 같다.&lt;br /&gt;&lt;br /&gt;&amp;lt; 커넥션이 연결되면 세션이 만들어지게 되고, 세션에 작업을 요청하면 프로세스가 실행 된다. &amp;gt;&lt;br /&gt;- 커넥션(Connection)은 클라이언트 프로세스와 데이터베이스 인스턴스간의 물리적인 TCP/IP 통신 경로를 의미한다.&lt;br /&gt;- 세션(Sessions)은 논리적인 개념으로 한 유저의 로그인 상태를 의미한다. 보통 커넥션과 1:1 으로 대응된다.&lt;br /&gt;- 프로세스(Process)는 실제 서버에 생성되는 운영체제 프로세스를 의미한다.&lt;br /&gt;&lt;br /&gt;다만  해당 DB가 자체적인 Connection Pool이나 Shared Server Mode 같은 걸 제공해준다면 한 커넥션에 여러개의 세션이 붙을 수도 있다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wn4Qn/btsCOEd0pJJ/ueg6Tl1Gp9BC9kRDPHvSh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wn4Qn/btsCOEd0pJJ/ueg6Tl1Gp9BC9kRDPHvSh0/img.png&quot; data-alt=&quot;Server의 Connection 객체 &amp;amp;lt;-&amp;amp;gt; 데이터베이스 Client Connection (TCP)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wn4Qn/btsCOEd0pJJ/ueg6Tl1Gp9BC9kRDPHvSh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWn4Qn%2FbtsCOEd0pJJ%2Fueg6Tl1Gp9BC9kRDPHvSh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1854&quot; height=&quot;760&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Server의 Connection 객체 &amp;lt;-&amp;gt; 데이터베이스 Client Connection (TCP)&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;위 그림을  사용자 입장에서 다시 그려보면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHCbQB/btsCyqH7V5f/6p6899CiGF1vKxMDlo2Esk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHCbQB/btsCyqH7V5f/6p6899CiGF1vKxMDlo2Esk/img.png&quot; data-alt=&quot;참고로 그림처럼 커넥션 풀을 사용하지 않는다면 DataSource 를 사용하지않고 DriverManager 만으로 간단히 연결 가능합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHCbQB/btsCyqH7V5f/6p6899CiGF1vKxMDlo2Esk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHCbQB%2FbtsCyqH7V5f%2F6p6899CiGF1vKxMDlo2Esk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;222&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참고로 그림처럼 커넥션 풀을 사용하지 않는다면 DataSource 를 사용하지않고 DriverManager 만으로 간단히 연결 가능합니다.&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: center;&quot;&gt;다만 어떤 방법을 사용하든 자바를 사용한다면 JDBC Driver 인터페이스를 이용해 데이터베이스에 접근하는건 동일합니다.&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: #0593d3;&quot;&gt;&amp;lt;DataSource 또는 DriverManager 직접 사용&amp;gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;당연히 쌩 JDBC 를 직접 사용해서 커넥션을 얻어와도 사용 가능&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&amp;lt;Spring JDBC Template&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내부 필드로 DataSource를 가지고 있다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&amp;lt;MyBatis (구 iBatis)&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SqlSessionFactory (spring mybatis는 SqlSessionFactoryBean) 에 DataSource를 등록해두고 세션인 SqlSessionTemplate 를 이용해 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&amp;lt; JPA &amp;gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;EntityManagerFactory에 DataSource를 등록해두고 세션인 EntityManager를 만들어 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&amp;lt; Spring Data JPA &amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구현체(SimpleJpaRepository)를 확인해보면 EntityManager를 스프링 빈으로 주입받아 사용한다. 해당 빈은 Spring AOP 프록시를 거쳐 현재 트랜잭션에 맞춰 적절한 커넥션에 할당된다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션과 커넥션 풀에 대하여 더 자세하게 알고싶다면 아래 글을 참고해주세요.&lt;/p&gt;
&lt;figure id=&quot;og_1703402618756&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;JDBC Connection 에 대한 이해, HikariCP 설정 팁&quot; data-og-description=&quot;JDBC가 무엇인지 아예 모른다면 [10분 테코톡] 코코닥의 JDBC 영상을 한번 보고 오시면 좋습니다.   데이터베이스의 시작은 결국 JDBC JVM을 사용한다면 스프링이건 JPA 를 쓰 건 결국 마지막엔 JDBC (J&quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/291&quot; data-og-url=&quot;https://jiwondev.tistory.com/291&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vX0Gf/hyUTEGCEgc/D9JJ4fo96j9pUO4Mtke0a1/img.png?width=800&amp;amp;height=500&amp;amp;face=0_0_800_500,https://scrap.kakaocdn.net/dn/mNMpO/hyUTBXp05E/3XaNiVK79hBTBAa6BzcyIk/img.png?width=800&amp;amp;height=500&amp;amp;face=0_0_800_500,https://scrap.kakaocdn.net/dn/gqdJ8/hyUPyH4aY2/J2cXgYEkl6F7K0uckTUDVk/img.png?width=2418&amp;amp;height=947&amp;amp;face=0_0_2418_947&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/291&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/291&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vX0Gf/hyUTEGCEgc/D9JJ4fo96j9pUO4Mtke0a1/img.png?width=800&amp;amp;height=500&amp;amp;face=0_0_800_500,https://scrap.kakaocdn.net/dn/mNMpO/hyUTBXp05E/3XaNiVK79hBTBAa6BzcyIk/img.png?width=800&amp;amp;height=500&amp;amp;face=0_0_800_500,https://scrap.kakaocdn.net/dn/gqdJ8/hyUPyH4aY2/J2cXgYEkl6F7K0uckTUDVk/img.png?width=2418&amp;amp;height=947&amp;amp;face=0_0_2418_947');&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;JDBC Connection 에 대한 이해, HikariCP 설정 팁&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JDBC가 무엇인지 아예 모른다면 [10분 테코톡] 코코닥의 JDBC 영상을 한번 보고 오시면 좋습니다.   데이터베이스의 시작은 결국 JDBC JVM을 사용한다면 스프링이건 JPA 를 쓰 건 결국 마지막엔 JDBC (J&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.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-origin-width=&quot;1986&quot; data-origin-height=&quot;1444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HiPmR/btsCTBhsrBL/0z5EMhuMCLnSYFA3ul1LMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HiPmR/btsCTBhsrBL/0z5EMhuMCLnSYFA3ul1LMK/img.png&quot; data-alt=&quot;보통 DriverManager를 직접 사용하는 경우는 거의 없습니다. 꼭 필요하다면 스프링의 DriverManagerDataSource 와 같이 사용하면 됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HiPmR/btsCTBhsrBL/0z5EMhuMCLnSYFA3ul1LMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHiPmR%2FbtsCTBhsrBL%2F0z5EMhuMCLnSYFA3ul1LMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1986&quot; height=&quot;1444&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;1444&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;보통 DriverManager를 직접 사용하는 경우는 거의 없습니다. 꼭 필요하다면 스프링의 DriverManagerDataSource 와 같이 사용하면 됩니다.&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  Transaction&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;a style=&quot;color: #0070d1;&quot; href=&quot;https://jiwondev.tistory.com/139&quot;&gt;2021.08.10 - [ 기본 지식/CS 기본지식] - 트랜잭션 격리수준(DB Isolation Level)&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://jiwondev.tistory.com/279&quot;&gt;2022.08.15 - [ 기본 지식/Java 기본지식] - 의식의 흐름대로 써보는 DB 트랜잭션 이야기&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Spring Transaction 추상화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; JDBC를 이용하면 DB의 상세 스펙을 몰라도 Java 코드로 사용할 수 있습니다만, 직접 사용하기엔 여러 단점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC를 사용하기 위해 try-catch-finally 같은 특정 패턴의 코드가 반복됩니다.&lt;/li&gt;
&lt;li&gt;Connection 같은 자원 이 누수되지 않도록 직접 관리해야합니다.&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;JDBC 관련 코드를 숨기려고해도, Transaction 설정 및 동기화 때문에 강제로 서비스코드와 섞이게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;JDBC의 &lt;span style=&quot;color: #0593d3;&quot;&gt;`throws SQLException`&lt;/span&gt; 을 어딘가에서 직접 처리해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제들을 해결하기위해 스프링에선 트랜잭션을 아래와 같이 추상화 해두었습니다.&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;1626&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9XqNv/btsCQ4xKUoB/Fk1HzQQIQzAsLl0pA6rHGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9XqNv/btsCQ4xKUoB/Fk1HzQQIQzAsLl0pA6rHGk/img.png&quot; data-alt=&quot;정확히는 PlatformTransactionManager &amp;amp;lt;- AbstractPlatformTransactionManager &amp;amp;lt;- { 구현체 } 로 이루어져있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9XqNv/btsCQ4xKUoB/Fk1HzQQIQzAsLl0pA6rHGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9XqNv%2FbtsCQ4xKUoB%2FFk1HzQQIQzAsLl0pA6rHGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;255&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정확히는 PlatformTransactionManager &amp;lt;- AbstractPlatformTransactionManager &amp;lt;- { 구현체 } 로 이루어져있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1704009049675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface PlatformTransactionManager{

    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException;


    void commit(TransactionStatus status) throws TransactionException;


    void rollback(TransactionStatus status) throws TransactionException;

}

public interface AbstractPlatformTransactionManager extends PlatformTransactionManager{
    /**
     * begin, commit, rollback 코드를 보면 실제 동작은 아래 메서드에 위임한다 
     * (EX commit()-&amp;gt; processCommit() -&amp;gt; doCommit())
     */    
    protected void doCommit(DefaultTransactionStatus status) throws TransactionException;

    protected void doRollback(DefaultTransactionStatus status) throws TransactionException;
	
    protected void doBegin(Object transaction, TransactionDefinition definition)
            throws TransactionException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 개발자는 PlatformTransactionManager 를 꺼내서 getTransaction, commit, rollback만 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 try-catch 없이 편리하게 쓰려고 스프링이 제공하는 템플릿 클래스(TransactionTemplate)로 래핑해서 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704014545064&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    // DataSource로 PlatformTransactionManager 생성
    return new DataSourceTransactionManager(dataSource);
}

@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
    // PlatformTransactionManager 으로 TransactionTemplate 생성 
    return new TransactionTemplate(transactionManager);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704014578001&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MyService {
    private final TransactionTemplate transactionTemplate;
    
    /** ⭐️ Java8 람다로 정의된 메서드를 쓰면 매우 깔끔합니다. */
    void myTransactionalLambda() {
        transactionTemplate.executeWithoutResult(status -&amp;gt; { /*..트랜잭션 로직..*/ });
        
        var result = transactionTemplate.execute(status -&amp;gt; { /*..트랜잭션 로직..*/ return null; });
    }
    
    /** 익명클래스로 사용할 땐 아래와 같습니다 */
    MyResultDto myTransactionalMethod() {
        return transactionTemplate.execute(new TransactionCallback&amp;lt;MyResultDto&amp;gt;() {
            @Override
            public MyResultDto doInTransaction(TransactionStatus status) {
                // 여기에 트랜잭션을 적용할 비즈니스 로직을 작성합니다.
                // 필요한 경우 status.setRollbackOnly(); 호출로 롤백을 지시할 수 있습니다.
                MyResultDto result = new MyResultDto();
                return result;
            }
        });
    }

    void myTransactionalMethod() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                // 여기에 트랜잭션을 적용할 비즈니스 로직을 작성합니다.
                // 필요한 경우 status.setRollbackOnly(); 호출로 롤백을 지시할 수 있습니다.
            }
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 내부 구현을 그림으로 그려보면 아래와 같습니다.&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;참고로 한 스레드가 여러 DB Connection을 물고있을 수는 있지만, 반대는 불가능합니다. 관리도 엄청 복잡해지고 스레드 경합문제도 있기때문입니다. 애초에 커넥션풀로 관리하는데 한 커넥션을 여러 스레드가 사용할 필요도 없구요.&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;2914&quot; data-origin-height=&quot;1284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tt500/btsCOAKDNAb/3mkLR1FLFeFaltgPFfDEtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tt500/btsCOAKDNAb/3mkLR1FLFeFaltgPFfDEtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tt500/btsCOAKDNAb/3mkLR1FLFeFaltgPFfDEtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTt500%2FbtsCOAKDNAb%2F3mkLR1FLFeFaltgPFfDEtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2914&quot; height=&quot;1284&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;1284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡해보일수 있는데 역할에 따라 분리해보면 생각보다 간단합니다. 예를 들어 DataSourceTransactionManager.doBegin() 를 까보면 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704016064489&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * doBegin: 트랜잭션을 시작합니다. 참고로 쌩 JDBC 에서는시작 메서드는 따로 없고 
   setAutoCommit(false) 로 트랜잭션을 시작합니다.
 * 
 * @param transaction 현재 트랜잭션에 관한 설정 정보 및 Connection 을 관리합니다. 
   구현체에 따라 TransactionObject 타입이 달라집니다.
 * (** Connection 자원 할당 및 회수는 TransactionSynchronizationManager 를 사용해서 처리합니다.)
 
 * @param definition 트랜잭션 전파, 격리, 타임아웃, 읽기 전용등의 동작 방식을 관리합니다.
 */
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    Connection con = null;
    
try {
    /**
     * 1. 사용중인 Connection이 없으면 새로 생성합니다.
     * 2. 사용중인 Connection이 다른 트랜잭션에 사용(동기화)중이라면 새로 생성합니다.
     *
     * 즉 이미 존재하는 Connection이 사용중이 아니라면 해당 커넥션을 사용합니다.
     */
    if (!txObject.hasConnectionHolder() ||
            txObject.getConnectionHolder().isSynchronizedWithTransaction()) {

        Connection newCon = obtainDataSource().getConnection();

        // 새롭게 생성된 Connection을 ConnectionHolder를 이용해 저장합니다.
        // ConnectionHolder 는 참조 카운트를 이용한 커넥션 생명주기 관리, rollback 지원, DB 트랜잭션 상태 저장등을 담당합니다.
        txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
    }


    // 다른 트랜잭션이 해당 Connection 을 사용하지 못하게 {동기화=true} 로 설정한 뒤 커넥션을 꺼내옵니다.
    txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
    con = txObject.getConnectionHolder().getConnection();

    // 트랜잭션 정의에 따라 격리 수준을 설정합니다. prepareConnectionForTransaction 까보면 그냥 JDBC 코드입니다.
    Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
    txObject.setPreviousIsolationLevel(previousIsolationLevel);
    txObject.setReadOnly(definition.isReadOnly());

    // 트랜잭션을 직접 관리하기때문에, autoCommit=true 으로 설정 된 경우 임시로 false 변경합니다.
    // 이는 setMustRestoreAutoCommit 에 기록해뒀다가 트랜잭션이 끝나는 시점에 기존 autoCommit 설정으로 원상 복귀됩니다.
    if (con.getAutoCommit()) {
        txObject.setMustRestoreAutoCommit(true);
        con.setAutoCommit(false);
    }

    // 커넥션 연결을 준비합니다. 까보면 그냥 JDBC로 &quot;SET TRANSACTION READ ONLY&quot; 같은 걸 설정하는 간단한 코드입니다.
    prepareTransactionalConnection(con, definition);
    txObject.getConnectionHolder().setTransactionActive(true);

    // 트랜잭션 정의에 따라 타임아웃을 설정합니다.
    int timeout = determineTimeout(definition);
    if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
        txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
    }

    // ConnectionHolder가 여기에서 첫 생성되었다면 TransactionSyncManager 에 등록해 자원을 관리합니다.
    // Map (key=Datasource, value=ConnectionHolder)
    if (txObject.isNewConnectionHolder()) {
        TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
    }
}

catch (Throwable ex) {
    // 예외 발생 시 연결을 해제하고 TransactionSyncManager 에 자원 해제를 요청합니다.
    if (txObject.isNewConnectionHolder()) {
        DataSourceUtils.releaseConnection(con, obtainDataSource());
        txObject.setConnectionHolder(null, false);
    }
    throw new CannotCreateTransactionException(&quot;Could not open JDBC Connection for transaction&quot;, ex);
}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  @Transactional은 어떻게 동작하는거에요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 스프링 AOP 를 트랜잭션에 적용한 도구입니다. 동작과정은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/152&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.03.09 - [  Spring Framework/Spring Core] - 자바 AOP의 모든 것(Spring AOP &amp;amp; AspectJ)&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;1️⃣ @EnableTransactionManagement 로 스프링 트랜잭션 AOP를 활성화합니다.&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;* @SpringBootApplication -&amp;gt; @EnableAutoConfiuration에 의해 EnableTransactionManagement 자동 활성화 됨&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;2️⃣ 활성화되면 @Import(TransactionManagementConfigurationSelector.class) 에 의해 빈으로 등록됩니다.&lt;br /&gt;우리는 스프링 프록시를 사용하므로 아래 코드에 따라 ProxyTransactionManagementConfiguration 가 빈으로 등록됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704016639354&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector&amp;lt;EnableTransactionManagement&amp;gt; {

	/** {스프링 프록시 객체} or {AspectJ 프록시 객체} 중 하나를 스프링 빈으로 등록합니다. */
	@Override
	protected String[] selectImports(AdviceMode adviceMode) {
		switch (adviceMode) {
			case PROXY:
				return new String[] {AutoProxyRegistrar.class.getName(),
						ProxyTransactionManagementConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {determineTransactionAspectClass()};
			default:
				return null;
		}
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3️⃣ ProxyTransactionManagementConfiguration 으로 트랜잭션 AOP에 필요한 구현 클래스들이 빈으로 등록됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/** Advice (프록시로 적용할 실동작 코드) 를 담당하는 Advisor 클래스, 스프링 컨테이너 AOP(BeanFactory)에서 적용됨*/
BeanFactoryTransactionAttributeSourceAdvisor;

/** 사용자 트랜잭션 설정(TransactionAttributeSource)를 읽어들이는 클래스, 어노테이션을 읽는 구현체 사용 */
AnnotationTransactionAttributeSource;

/** PointCut(적용 조건), 즉 invoke() 메서드를 구현해서 AOP을 적용할 조건을 지정하는 클래스 */
TransactionInterceptor;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3258&quot; data-origin-height=&quot;2440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LkYhQ/btsCWMJpi7b/BK0IvR8RGpKyNcCxJz0oA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LkYhQ/btsCWMJpi7b/BK0IvR8RGpKyNcCxJz0oA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LkYhQ/btsCWMJpi7b/BK0IvR8RGpKyNcCxJz0oA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLkYhQ%2FbtsCWMJpi7b%2FBK0IvR8RGpKyNcCxJz0oA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3258&quot; height=&quot;2440&quot; data-origin-width=&quot;3258&quot; data-origin-height=&quot;2440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transactionInterceptor 코드를 보시면 &lt;span style=&quot;color: #ef5369;&quot;&gt;`setTransactionManager(this.txManager)`&lt;/span&gt; 를 등록하는 부분이 있는데, 여기에 PlatformTransactionManager 가 설정됩니다.&amp;nbsp;이제 @Transactional 을 사용하면 AOP를 통해 트랜잭션이 걸리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;설정부분이 복잡하지만 동작은 생각보다 간단합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내가 원하는 메서드에 @Transactional 명시&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jiwondev.tistory.com/152&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;클래스가 빈으로 등록되는 시점에 Spring BeanPostProcessor 에 의한 AOP 프록시 생성&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PlatformTransactionManager 으로 메서드 시작전, 후에 트랜잭션 코드 집어넣기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  트랜잭션 전파옵션..? 이런게 JDBC에 있었던가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 격리레벨(Transaction Isolation)은 JDBC에서도 손쉽게 설정가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704018345721&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그런데 전파옵션 (Transaction Propagation)은 설정에 따라&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기존 트랜잭션을 연장하거나, 새로운 트랜잭션을 만드는 등의 기능인데&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 당연히 JDBC 자체적으로 제공해주는 기능은 아닙니다. TransactionDefinition 설정값에 따라 스프링이 구현한 내용입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bX8Ltl/btsCMwhIKSE/QNXDCFNDEBtR92CCgMWrk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bX8Ltl/btsCMwhIKSE/QNXDCFNDEBtR92CCgMWrk0/img.png&quot; data-alt=&quot;@Transactional 이나 TxTemplate에 명시한걸 읽어서 전파 옵션을 TransactionDefinition 객체로 전달합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bX8Ltl/btsCMwhIKSE/QNXDCFNDEBtR92CCgMWrk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbX8Ltl%2FbtsCMwhIKSE%2FQNXDCFNDEBtR92CCgMWrk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2304&quot; height=&quot;382&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@Transactional 이나 TxTemplate에 명시한걸 읽어서 전파 옵션을 TransactionDefinition 객체로 전달합니다.&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;구현 내용은 까봐야겠지만, JDBC의 savePoint 등을 적절하게 활용하면 불가능한건 아닙니다.&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;1056&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daHqB2/btsCMnLRvz7/FQczRToRI8Q5OawIORxOx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daHqB2/btsCMnLRvz7/FQczRToRI8Q5OawIORxOx0/img.png&quot; data-alt=&quot;출처&amp;amp;amp;nbsp;https://jeong-pro.tistory.com/228&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daHqB2/btsCMnLRvz7/FQczRToRI8Q5OawIORxOx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaHqB2%2FbtsCMnLRvz7%2FFQczRToRI8Q5OawIORxOx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1056&quot; height=&quot;467&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처&amp;amp;nbsp;https://jeong-pro.tistory.com/228&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;참고로 JDBC savePoint 는 아래와 같이 사용하는 기능입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704018728886&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// try-with-resources 문을 사용하여 자동으로 리소스 해제
try (Connection conn = DriverManager.getConnection(url, user, password)) {
    conn.setAutoCommit(false);

    // 첫 번째 업데이트
    try (PreparedStatement pstmt1 = conn.prepareStatement(sql1)) { pstmt1.executeUpdate(); }

    // Savepoint 설정
    Savepoint savepoint1 = conn.setSavepoint(&quot;Savepoint1&quot;);

    // 두 번째 업데이트
    try (PreparedStatement pstmt2 = conn.prepareStatement(sql2)) { pstmt2.executeUpdate(); }

    conn.commit();
} catch (SQLException e) {
    try {
        if (savepoint1 != null) {
            // Savepoint가 있다면, 거기로 롤백 ( savePoint 이후 작업만 롤백됨 )
            conn.rollback(savepoint1);
            
            conn.commit(); // 다시 커밋
        }
    } catch (SQLException se) {
       /* .. */
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이보면 좋은 글&lt;br /&gt;&lt;a href=&quot;https://jiwondev.tistory.com/154&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.18 - [  Spring Framework/Spring Core] - @Transactional 의 동작원리, 트랜잭션 매니저&lt;/a&gt;&lt;/p&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://jiwondev.tistory.com/155&quot;&gt;2021.08.18 - [  Spring Framework/Spring Core] - DB와 @Transactional을 사용할때 자주 나오는 실수&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  자바의 Exception 처리&lt;/h2&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: #0593d3;&quot;&gt;Throwable&lt;/span&gt;: throw를 사용할 수 있는 인터페이스&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;Throwable-&amp;gt;Error&lt;/span&gt;: 개발자가 처리할 수 없는 JVM 자체 에러 (메모리부족, 시스템 OS 오류)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;Throwable-&amp;gt;Exception&lt;/span&gt;: 개발자가 처리할 수 있는 에러&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;Throwable-&amp;gt;Exception -&amp;gt; RuntimeException&lt;/span&gt;: 언체크드 예외, 즉 런타임에서 처리해야하는 예외&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxNZRe/btsCMpbTaRp/ZrZU8NmOsLEfyY1rhfz5yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxNZRe/btsCMpbTaRp/ZrZU8NmOsLEfyY1rhfz5yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxNZRe/btsCMpbTaRp/ZrZU8NmOsLEfyY1rhfz5yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxNZRe%2FbtsCMpbTaRp%2FZrZU8NmOsLEfyY1rhfz5yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;410&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 예외가 발생하면 기본적으로 상위 메서드로 던져진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝까지 처리하지않으면 main() 스레드까지 예외가 전달되고 이는 시스템이 종료된다. &lt;span style=&quot;color: #0593d3;&quot;&gt;웹 서버라 tomcat 등을 사용할 경우 main 까지 가지않고 WAS가 받아서 기본값으로 처리한다.&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;단순히 설명만 들으면 RuntimeException을 사용하지않고, 전부 체크 예외 (throws 로 명시하고 try-catch가 강제되는 예외)를 사용하면 편할 것 같지만, 특정 Exception 객체에 의존성이 생기게되고 코드 자체가 더러워진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/63t4Y/btsCUZI0nAU/75yr8jyLe90BFqz0pHQm11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/63t4Y/btsCUZI0nAU/75yr8jyLe90BFqz0pHQm11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/63t4Y/btsCUZI0nAU/75yr8jyLe90BFqz0pHQm11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F63t4Y%2FbtsCUZI0nAU%2F75yr8jyLe90BFqz0pHQm11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;616&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보통 웹 서버의 경우 RuntimeException을 많이 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;실제로 체크 익셉션으로 예외처리를 강제해봐야 코드상으로 할 수 있는게 없어서 예외를 try{..} catch 로 아무 처리도 하지않고 무시하는 개발자도 많았었다. 개발자가 예상하지 못한 에러는 최상위에서 ControllerAdvice (또는 main 코드) 에서만 예외를 처리해주는게 맞다.&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;1584&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pqByQ/btsCMyGxW6R/FMrKkycB4QM8VgUfXZPfyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pqByQ/btsCMyGxW6R/FMrKkycB4QM8VgUfXZPfyk/img.png&quot; data-alt=&quot;보통 RuntimeException을 상속한 MyCommonException 등을 만들어서 많이 사용한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pqByQ/btsCMyGxW6R/FMrKkycB4QM8VgUfXZPfyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpqByQ%2FbtsCMyGxW6R%2FFMrKkycB4QM8VgUfXZPfyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1584&quot; height=&quot;784&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;보통 RuntimeException을 상속한 MyCommonException 등을 만들어서 많이 사용한다.&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://jiwondev.tistory.com/84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.07 - [ Backend/Java] - #7 Exception (예외처리)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704019248220&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;#7 Exception (예외처리)&quot; data-og-description=&quot;자바에서의 오류, 예외 자바에서 예외, 오류는 java.lang.Throwable 클래스를 상속받으며 구현하며 3가지 종류로 나뉜다. Error 자바 프로그램 밖에서 발생한 예외. 프로세스에 영향, 더 이상 실행 불가&quot; data-og-host=&quot;jiwondev.tistory.com&quot; data-og-source-url=&quot;https://jiwondev.tistory.com/84&quot; data-og-url=&quot;https://jiwondev.tistory.com/84&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b8ILwX/hyUXVBEZkp/m4NUo1tkkiAa4Th3MHdvs1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/dPAIks/hyUXM5MGFm/TkQk3DPxVxpCqVQSF6l0c0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/xGeot/hyUXXlVOrr/mV1xx2Au2Y6C6CuFfAy6k1/img.png?width=622&amp;amp;height=422&amp;amp;face=0_0_622_422&quot;&gt;&lt;a href=&quot;https://jiwondev.tistory.com/84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jiwondev.tistory.com/84&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b8ILwX/hyUXVBEZkp/m4NUo1tkkiAa4Th3MHdvs1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/dPAIks/hyUXM5MGFm/TkQk3DPxVxpCqVQSF6l0c0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/xGeot/hyUXXlVOrr/mV1xx2Au2Y6C6CuFfAy6k1/img.png?width=622&amp;amp;height=422&amp;amp;face=0_0_622_422');&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;#7 Exception (예외처리)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;자바에서의 오류, 예외 자바에서 예외, 오류는 java.lang.Throwable 클래스를 상속받으며 구현하며 3가지 종류로 나뉜다. Error 자바 프로그램 밖에서 발생한 예외. 프로세스에 영향, 더 이상 실행 불가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jiwondev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;  스프링의 DB Exception 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링도 이와 비슷하게 DB에서 발생하는 에러를 추상화해두었다. ( org.springframework.dao.DataAccessException )&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;여담으로 DB 뿐만 아니라 스프링의 다양한 기능이 예외를 이런식으로 처리한다.&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;1612&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yMcBj/btsCSBBZY7D/7s9O9PBu6BiyCGHy4VmSE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yMcBj/btsCSBBZY7D/7s9O9PBu6BiyCGHy4VmSE1/img.png&quot; data-alt=&quot;Transient는 일시적이라는 의미이다. 즉 DB 나 SQL 자체 문제가 아닌 {타임아웃, 락}과 같은 예외가 여기에 속한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yMcBj/btsCSBBZY7D/7s9O9PBu6BiyCGHy4VmSE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyMcBj%2FbtsCSBBZY7D%2F7s9O9PBu6BiyCGHy4VmSE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;482&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Transient는 일시적이라는 의미이다. 즉 DB 나 SQL 자체 문제가 아닌 {타임아웃, 락}과 같은 예외가 여기에 속한다.&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;예외가 정의 되어있다한들, 내가 모든 DB의 익셉션을 DataAccessException 으로 변환해주어야하는 번거로움은 그대로이다. 그래서 스프링에선 SQLExceptionTranslator 객체를 따로 만들어서 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);

// exTranslator.translate(String task, String sql, Exception e)
// 참고로 String task 는 일종의 주석이다. 예를 들어 하이버네이트의 경우 &quot;Hibernate operation: &quot; 으로 전달한다.
DataAccessException resultEx = exTranslator.translate(&quot;select&quot;, sql, e);

// 변환한 예외를 그대로 던질 수도 있다.
throw resultEx;&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2364&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqJOlh/btsCQsr0xxH/FT3KdXHMOZGlYbkRiGC2bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqJOlh/btsCQsr0xxH/FT3KdXHMOZGlYbkRiGC2bk/img.png&quot; data-alt=&quot;실제 JDBCTemplate 등을 까보면, 예외가 터졌을 때 등록된 ExceptionTranslator로 예외를 변환한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqJOlh/btsCQsr0xxH/FT3KdXHMOZGlYbkRiGC2bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqJOlh%2FbtsCQsr0xxH%2FFT3KdXHMOZGlYbkRiGC2bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2364&quot; height=&quot;1032&quot; data-origin-width=&quot;2364&quot; data-origin-height=&quot;1032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 JDBCTemplate 등을 까보면, 예외가 터졌을 때 등록된 ExceptionTranslator로 예외를 변환한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링으로 DB를 사용해봤다면 생각보다 에러 종류가 상세하게 구분되어있는걸 알 수 있는데, 이는 스프링이 각 DB 가 반환하는  에러별로 class SqlErrorCodes 객체를 정의해두었기 때문이다.  &lt;span style=&quot;color: #0593d3;&quot;&gt;이는 org.springframework.jdbc.support.sql-error-codes.xml 파일을 통해 빈이 생성된다.&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;3582&quot; data-origin-height=&quot;1588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DKJ3F/btsCQoDasve/oX74erKJe2Zl0kJ5PvMEB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DKJ3F/btsCQoDasve/oX74erKJe2Zl0kJ5PvMEB1/img.png&quot; data-alt=&quot; 내부 코드를 까보면 SQLErrorCodeSQLExceptionTranslator 가 등록된 예외를 읽어서 switch 분기를 쳐준다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DKJ3F/btsCQoDasve/oX74erKJe2Zl0kJ5PvMEB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDKJ3F%2FbtsCQoDasve%2FoX74erKJe2Zl0kJ5PvMEB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3582&quot; height=&quot;1588&quot; data-origin-width=&quot;3582&quot; data-origin-height=&quot;1588&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 내부 코드를 까보면 SQLErrorCodeSQLExceptionTranslator 가 등록된 예외를 읽어서 switch 분기를 쳐준다.&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; 로그를 남기고 DataAccessException을 상속한 UncategorizedSQLException 을 던진다. 물론 이는 구현체에 따라 다를 수 있다.&lt;/p&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: #0593d3;&quot;&gt;어찌보면 당연한거지만, 해당 기능을 편하게 쓰려면 Spring에서 제공해주는 도구를 사용해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;Spring의 의존성까지 제거하고 싶다면 이를 참조해서 직접 자바코드로 구현해도 되지만, 대부분의 서비스는 DB 를 추상화한 부분을 도메인 로직처럼 순수한 Java (POJO)로 가져갈 필요는 없다. Spring Framework를 사용하기로 결정했다면 대부분 크게 신경쓰지않아도 되는 부분이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  여담&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 스프링이 이 모든걸 다해준 JdbcTemplte 을 사용하더라도 사실상 실제 프로덕션 코드로 사용하다보면 그렇게 엄청 편리하지는 않다.&lt;/p&gt;
&lt;pre id=&quot;code_1704022248844&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
class EmployeeRepository(private val jdbcTemplate: JdbcTemplate) {

    fun findEmployeesWithJoin(minSalary: Double, maxSalary: Double, departmentName: String, positionTitle: String): List&amp;lt;Employee&amp;gt; {
        val sql = &quot;&quot;&quot;
            SELECT 
                e.*, 
                d.id as department_id, d.name as department_name, 
                p.id as position_id, p.title as position_title
            FROM employees e
            INNER JOIN departments d ON e.department_id = d.id
            INNER JOIN positions p ON e.position_id = p.id
            WHERE e.salary BETWEEN ? AND ?
              AND d.name = ?
              AND p.title = ?
            ORDER BY e.lastName, e.firstName
        &quot;&quot;&quot;

        return jdbcTemplate.query(
            sql,
            arrayOf(minSalary, maxSalary, departmentName, positionTitle),
            EmployeeRowMapper()
        )
    }
}

class EmployeeRowMapper : RowMapper&amp;lt;Employee&amp;gt; {
    override fun mapRow(rs: ResultSet, rowNum: Int): Employee {
        return Employee(
            id = rs.getLong(&quot;id&quot;),
            firstName = rs.getString(&quot;firstName&quot;),
            lastName = rs.getString(&quot;lastName&quot;),
            // 기타 Employee 필드
            department = Department(
                id = rs.getLong(&quot;department_id&quot;),
                name = rs.getString(&quot;department_name&quot;)
                // 기타 Department 필드
            ),
            position = Position(
                id = rs.getLong(&quot;position_id&quot;),
                title = rs.getString(&quot;position_title&quot;)
                // 기타 Position 필드
            )
            // 기타 필드
        )
    }
}&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;물론 ORM (ex JPA + QueryDsl 조합)으로 대부분 해결되지만, 특정 DB에 종속된 고급 쿼리나 통계 쿼리의 경우 그게 불가능 한 경우도 많다. &lt;span style=&quot;color: #0593d3;&quot;&gt;그런 쿼리엔 JPA 가 똑바로 생성하는지 믿기 힘든 것도 있고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이런 류의 기술로 여러가지를 찾아봤었는데, 전부 장단점이 있어 애매했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 DB로 해결하지않고 검색이면 Elasticsearch, 대용량 처리는 Hadoop에 밀어넣고 Google BigQuery로 사용하는 방법도 있는데 그건 기업이 큰 경우에만 가능한 방법이니 잠깐 밀어두고, 가능한 방법들을 정리하면 아래와 같다.&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;Spring JDBC Template (위 예제코드)&lt;/li&gt;
&lt;li&gt;Jooq&lt;span style=&quot;color: #0593d3;&quot;&gt; ( native sql을 마치 JPA처럼 사용하는 라이브러리 )&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;MyBatis &lt;span style=&quot;color: #0593d3;&quot;&gt;( 선호하지는 않지만 Java8 이하를 사용한다면 유일한 선택지   )&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;JPA + 복잡한 쿼리만 EntityManager.createNativeQuery 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 고민을 나만 한게 아닌지 SpringBoot 3.0 에 JDBC Client 라는게 추가되었더라. 생각보다 쓸만해보여서 나중에 써보고 글로 후기를 써볼 예정이다.&lt;/p&gt;
&lt;pre id=&quot;code_1704023101033&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
class EmployeeRepository(private val jdbcAggregateOperations: JdbcAggregateOperations) {

    fun findEmployeeById(employeeId: Long): Employee? {
        return jdbcAggregateOperations.findById(employeeId, Employee::class.java)
    }

    fun findAllEmployees(): List&amp;lt;Employee&amp;gt; {
        return jdbcAggregateOperations.findAll(Employee::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Jpa에서 사용하던 기능들중, Spring Data에만 의존하고 있는 기능들도 많다. 아래와 같이 Spring Data의 Criteria를 활용하는 방법도 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KCrUn/btsCMx1W1IB/wygDkKSiNLoo8jiZNjx4Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KCrUn/btsCMx1W1IB/wygDkKSiNLoo8jiZNjx4Pk/img.png&quot; data-alt=&quot;출처-&amp;amp;amp;nbsp;www.youtube.com/watch?v=RKxQWQJftAg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KCrUn/btsCMx1W1IB/wygDkKSiNLoo8jiZNjx4Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKCrUn%2FbtsCMx1W1IB%2FwygDkKSiNLoo8jiZNjx4Pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;327&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처-&amp;amp;nbsp;www.youtube.com/watch?v=RKxQWQJftAg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1704023333743&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** Spring Data의 Criteria 활용 */
@Repository
class EmployeeRepository(private val jdbcAggregateTemplate: JdbcAggregateTemplate) {

    fun findEmployeesWithJoin(minSalary: Double, maxSalary: Double, departmentName: String, positionTitle: String): List&amp;lt;Employee&amp;gt; {
        val query = Query.query(
            Criteria.where(&quot;e.salary&quot;).between(minSalary, maxSalary)
                .and(&quot;d.name&quot;).`is`(departmentName)
                .and(&quot;p.title&quot;).`is`(positionTitle)
        ).sort(Sort.by(&quot;e.lastName&quot;).and(Sort.by(&quot;e.firstName&quot;)))

        return jdbcAggregateTemplate.find(query, Employee::class.java, &quot;employees e&quot;)
            .innerJoin(&quot;departments d&quot;, &quot;e.department_id = d.id&quot;)
            .innerJoin(&quot;positions p&quot;, &quot;e.position_id = p.id&quot;)
            .all()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금하다면 아래 링크를 한번쯤 꼭 보는걸 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://www.baeldung.com/spring-6-jdbcclient-api&quot;&gt;설명 -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://www.baeldung.com/spring-6-jdbcclient-api&quot;&gt;https://www.baeldung.com/spring-6-jdbcclient-api&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=RKxQWQJftAg&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;유튜브 - 하찮은오후님 JdbcClient와&amp;nbsp;record를&amp;nbsp;결합하면&amp;nbsp;시너지가&amp;nbsp;커집니다.&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=RKxQWQJftAg&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/Ki3fl/hyUXWAz1dj/4yibcu4sM5R5HGAjtACpC0/img.jpg?width=640&amp;amp;height=480&amp;amp;face=0_0_640_480&quot; data-video-width=&quot;640&quot; data-video-height=&quot;480&quot; data-video-origin-width=&quot;640&quot; data-video-origin-height=&quot;480&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/RKxQWQJftAg&quot; width=&quot;640&quot; height=&quot;480&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  2025/JDBC &amp;amp; JPA</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/294</guid>
      <comments>https://jiwondev.tistory.com/294#entry294comment</comments>
      <pubDate>Sun, 31 Dec 2023 20:20:11 +0900</pubDate>
    </item>
    <item>
      <title>JDBC Connection 에 대한 이해, HikariCP 설정 팁</title>
      <link>https://jiwondev.tistory.com/291</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC가 무엇인지 아예 모른다면 &lt;a href=&quot;https://www.youtube.com/watch?v=ONYVhJGl48U&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;[10분 테코톡] 코코닥의 JDBC&lt;/a&gt; 영상을 한번 보고 오시면 좋습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2418&quot; data-origin-height=&quot;947&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oEmQ3/btsAJ128WNE/dlCG9bSgb8j1SdFCkHtYJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oEmQ3/btsAJ128WNE/dlCG9bSgb8j1SdFCkHtYJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oEmQ3/btsAJ128WNE/dlCG9bSgb8j1SdFCkHtYJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoEmQ3%2FbtsAJ128WNE%2FdlCG9bSgb8j1SdFCkHtYJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2418&quot; height=&quot;947&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2418&quot; data-origin-height=&quot;947&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  데이터베이스의 시작은 결국 JDBC&amp;nbsp;&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JVM을 사용한다면 스프링이건 JPA 를 쓰 건 결국 마지막엔 JDBC&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;(Java Database Connectivity API)&lt;/span&gt;를 이용하여 DB 와 통신하게 됩니다. 참고로&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;JDBC Driver는 인터페이스 일 뿐 실제 구현체는 공식 DB 벤더사(Oracle, MS..)에서 제공합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;만약 나만의 Driver를 구현하고 싶다면 동일하게 JDBC Driver 구현체를 만들어서 등록하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKZ7Cg/btsAEoYTYcq/l8heDQUfvkHJxTa9iB5Xuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKZ7Cg/btsAEoYTYcq/l8heDQUfvkHJxTa9iB5Xuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKZ7Cg/btsAEoYTYcq/l8heDQUfvkHJxTa9iB5Xuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKZ7Cg%2FbtsAEoYTYcq%2Fl8heDQUfvkHJxTa9iB5Xuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;526&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre id=&quot;code_1703768026335&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;try {
    // JDBC 4.0 부터는 META-INF/services/java.sql.Driver를 읽어 DriverManager를 미리 생성해줍니다.
    // 물론 Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;) 를 호출해서 직접 DriverManager에 추가할 수도 있습니다.
    connection = DriverManager.getConnection(jdbcUrl, username, password)
    println(&quot;Database connection established.&quot;)

    // Statement 생성
    statement = connection.createStatement()

    // SQL 쿼리 실행
    val sql = &quot;SELECT * FROM your_table&quot;
    resultSet = statement.executeQuery(sql)

    // 결과 처리
    while (resultSet.next()) {
        // 첫 번째 컬럼 값을 String으로 읽기
        val firstColumnValue = resultSet.getString(1)
        println(firstColumnValue)
    }
} catch (e: Exception) {
    e.printStackTrace()
} finally {
    // DB 커넥션을 닫아줘야 합니다. Java7 부터는 try-resource 문 (kotlin 에선 use)를 사용하면 편합니다.
    resultSet?.close()
    statement?.close()
    connection?.close()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;  DB 트랜잭션은 어떻게 거는 걸까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 DBMS 는 AutoCommit 트랜잭션 모드와 명시적인 트랜잭션 모드(BEGIN-COMMIT-ROLLBACK) 둘 다 제공합니다.&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;당연한거지만 DB에서 모든 작업은 트랜잭션 내에서 이루어집니다. 트랜잭션을 걸지 않았더라도 AutoCommit에 의해 작업 완료 후 Commit이 실행 됩니다.&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; JDBC의 경우  트랜잭션 범위를 지정할 때 별도의 API, 예를 들어 BeginTransaction() 같은 메서드를 따로 제공하지않습니다. 대신  Connection.setAutoCommit( 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;이 때 한가지 유의해야할 점은, setAutoCommit을 바꿀 때 JDBC 내부에만 뚝-딱 적용되는게 아니라 DBMS에 쿼리로 요청해야한다는 점입니다. 이를 무의미하게 계속 변경한다면 필요 이상의 트래픽과 DB 부하가 발생될 수 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;물론 스프링을 쓴다면 DataSourceTranscationManager 에서 이미 최적화 되어있고 HikariDataSource 에서도 autoCommit을 관리해주기 때문에 개발자가 신경 쓸 일은 없긴합니다.&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;1810&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m8zxw/btsAy7KtQwR/OnZr8z57w2ghhSjDVMIyqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m8zxw/btsAy7KtQwR/OnZr8z57w2ghhSjDVMIyqK/img.png&quot; data-alt=&quot;BEGIN(deferred)는 실제 쿼리를 실행 요청을 하는 시점에 &amp;quot;BEGIN&amp;quot;  을 전송한다는 의미입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m8zxw/btsAy7KtQwR/OnZr8z57w2ghhSjDVMIyqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm8zxw%2FbtsAy7KtQwR%2FOnZr8z57w2ghhSjDVMIyqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1810&quot; height=&quot;596&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;BEGIN(deferred)는 실제 쿼리를 실행 요청을 하는 시점에 &quot;BEGIN&quot;  을 전송한다는 의미입니다.&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;만약 Reader, Writer DB를 분리해서 사용한다면 아래와 같이 기본값을 다르게 설정해주기도 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700537808508&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    maindb:
      writer:
        driver-class-name: org.postgresql.Driver
        auto-commit: false # writer는 직접 트랜잭션을 걸일이 많으니 기본값 false
        connection-timeout: 3000
      reader:
        driver-class-name: org.postgresql.Driver
        auto-commit: true # read-only는 트랜잭션을 걸 일이 없으니 기본값 true
        connection-timeout: 3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;JDBC API는 어떻게 동작할까요?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTKtNW/btsACkCvPpi/bQwwHWaUBzYgA4qAJwJ6SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTKtNW/btsACkCvPpi/bQwwHWaUBzYgA4qAJwJ6SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTKtNW/btsACkCvPpi/bQwwHWaUBzYgA4qAJwJ6SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTKtNW%2FbtsACkCvPpi%2FbQwwHWaUBzYgA4qAJwJ6SK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;131&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC API 를 자세하게 살펴보면 크게 JDBC Driver와 DataSource로 나눌 수 있습니다.&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고로 Repository(DAO, Data Access Object)는 개발자가 직접 작성하는 코드를 의미합니다.&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;JDBC Driver 인터페이스는 실제 데이터베이스와 네트워크 연결을 담당합니다.&lt;/li&gt;
&lt;li&gt;DataSource 인터페이스는 각종 설정 및 커넥션 풀을 담당합니다.&lt;/li&gt;
&lt;li&gt;참고로 아무런 설정없이 연습용으로 단순 연결을 해볼 때는 DataSource 대신 DriverManager  를&amp;nbsp; 직접 사용할 수도 있습니다.&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-origin-width=&quot;567&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0Y93U/btsAF3mmqak/IEkJobWTQEWS2p5fL9NIoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0Y93U/btsAF3mmqak/IEkJobWTQEWS2p5fL9NIoK/img.png&quot; data-alt=&quot;이미지 출처&amp;amp;amp;nbsp;https://netmarble.engineering/jdbc-timeout-for-game-server&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0Y93U/btsAF3mmqak/IEkJobWTQEWS2p5fL9NIoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0Y93U%2FbtsAF3mmqak%2FIEkJobWTQEWS2p5fL9NIoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;471&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처&amp;amp;nbsp;https://netmarble.engineering/jdbc-timeout-for-game-server&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;b&gt;JDBC Driver&lt;/b&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;3212&quot; data-origin-height=&quot;1610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgJnCd/btsAHZ6g5TS/EK5lsR7X1rlycXfhnhDWO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgJnCd/btsAHZ6g5TS/EK5lsR7X1rlycXfhnhDWO1/img.png&quot; data-alt=&quot; DriverManager에 등록된 클래스들은 JDBC Driver 구현체가 생성될 때 static {...} 으로 런타임에 등록됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgJnCd/btsAHZ6g5TS/EK5lsR7X1rlycXfhnhDWO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgJnCd%2FbtsAHZ6g5TS%2FEK5lsR7X1rlycXfhnhDWO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;334&quot; data-origin-width=&quot;3212&quot; data-origin-height=&quot;1610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; DriverManager에 등록된 클래스들은 JDBC Driver 구현체가 생성될 때 static {...} 으로 런타임에 등록됩니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 자바에서 Driver 인터페이스로 제공됩니다. 직접 사용하지않고 DataSource를 통해 사용하며 이는 특정 DB와 실질적인 통신을 구현한 부분입니다. Driver 구현체는 공식 벤더사(ex Oracle, MS)가 구현한 최적화된 코드를 이용합니다.&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;1728&quot; data-origin-height=&quot;1732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eLBE5B/btsAxanua0W/cohJrpTr26bjTODJtR3Bn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eLBE5B/btsAxanua0W/cohJrpTr26bjTODJtR3Bn0/img.png&quot; data-alt=&quot;JDBC 4.0 부터는 직접 등록 안해도, META-INF/services/java.sql.Driver 경로에 두면 JDBC Driver가 실행될 때 자동으로 등록합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eLBE5B/btsAxanua0W/cohJrpTr26bjTODJtR3Bn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeLBE5B%2FbtsAxanua0W%2FcohJrpTr26bjTODJtR3Bn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;601&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1732&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JDBC 4.0 부터는 직접 등록 안해도, META-INF/services/java.sql.Driver 경로에 두면 JDBC Driver가 실행될 때 자동으로 등록합니다.&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;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DataSource&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;986&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkA6Hz/btsAH0D7zal/hs8tXbAYCm2jQpbp0BPZg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkA6Hz/btsAH0D7zal/hs8tXbAYCm2jQpbp0BPZg0/img.png&quot; data-alt=&quot;실제 구현체에서 DriverManager로 직접 커넥션을 가져오든, 커넥션 풀이나 이미 있는 DataSource를 래핑해서 사용하든 추상화해서 사용하기에 아무 상관없습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkA6Hz/btsAH0D7zal/hs8tXbAYCm2jQpbp0BPZg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkA6Hz%2FbtsAH0D7zal%2Fhs8tXbAYCm2jQpbp0BPZg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;314&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 구현체에서 DriverManager로 직접 커넥션을 가져오든, 커넥션 풀이나 이미 있는 DataSource를 래핑해서 사용하든 추상화해서 사용하기에 아무 상관없습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;* 당연한거지만 DriverManager 내부 코드에는 DataSource 는 존재하지 않습니다. 그래서 이미 존재하는 커넥션 풀 객체를 적용할 수 없는데, Spring을 사용한다면 DriverManagerDataSource를 이용하면 손쉽게 DriverManager 에 커넥션풀을 붙여서 사용할 수 있습니다.&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;자바에서 DataSource 인터페이스로 제공됩니다. 이는 JDBC Driver가 가져온 DB 커넥션 과 커넥션 풀을 관리하는 기능을 제공합니다. DataSource의 구현체 또한 DB 벤더사 또는 서드파티사를 통해 제공됩니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 87px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px; text-align: center;&quot;&gt;라이브러리 이름&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px; text-align: center;&quot;&gt;JDBC DataSource 패키지 경로(=이름)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Commons DBCP ( * Apache DBCP )&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/5102792&quot;&gt;https://d2.naver.com/helloworld/5102792&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;org.apache.commons.dbcp2.BasicDataSource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Tomcat JDBC (* 톰캣 내장 DBCP)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;org.apache.tomcat.jdbc.pool.DataSource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;DRUID ( * 알리바바 DBCP )&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;com.alibaba.druid.pool.DruidDataSource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;HikariCP ( * Spring 기본 DBCP )&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;com.zaxxer.hikari.HikariDataSource&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&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;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;Connection Time out은 왜 발생하는걸까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC Driver도 결국 네트워크를 통해 DB 서버와 통신하는 클라이언트입니다. 보통 소켓을 이용한 TCP/IP 연결을 사용하기 때문에&amp;nbsp; 읽기 도중 Connection Timeout, Socket Timeout이 발생할 수 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;DB 구현체마다 다르겠지만 보통 커넥션이 끊어진 경우 실행중인 쿼리가 취소되고 트랜잭션이 롤백됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;Connection Timeout&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 최초 연결을 맺는 시간(임계치, threshold)를 초과된 경우&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;Socket Timeout&lt;span&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-origin-width=&quot;1320&quot; data-origin-height=&quot;1202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clLgF8/btsAw5mf06g/vAMESRDu6JBNL486blXIKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clLgF8/btsAw5mf06g/vAMESRDu6JBNL486blXIKK/img.png&quot; data-alt=&quot;이미지 출처&amp;amp;amp;nbsp;https://netmarble.engineering/jdbc-timeout-for-game-server&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clLgF8/btsAw5mf06g/vAMESRDu6JBNL486blXIKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclLgF8%2FbtsAw5mf06g%2FvAMESRDu6JBNL486blXIKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;546&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;1202&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처&amp;amp;nbsp;https://netmarble.engineering/jdbc-timeout-for-game-server&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;여기에 추가로 JDBC Driver에서 Statement Timeout 까지 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SocketTime이 물리적인 연결에서 네트워크 장애를 의미한다면, Statement Timeout은 DB에서 수행되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나의 SQL Statement 수행시간에 대한 타임 아웃을 지정할 수 있습니다. &lt;span style=&quot;color: #0593d3;&quot;&gt;여담으로 Spring TransactionManager를 사용한다면 트랜잭션 timeout 도 추가로 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;참고로  SQL 실행시간에 제한을 걸기위해 Statement Timeout을 설정할 경우, 당연히 Socket Timeout 을 더 크게 잡아줘야합니다. 어차피 소켓이 끊기면 내부에서 어떻게 설정했던 해당 트랜잭션은 롤백되니까요.&amp;nbsp;&lt;/blockquote&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;737&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNmoO/btsAxisrse0/9ZqAdcvtB2HuUNyEfA6ic0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNmoO/btsAxisrse0/9ZqAdcvtB2HuUNyEfA6ic0/img.png&quot; data-alt=&quot;Connection -&amp;amp;gt; Statement (StatementTimeout 발생지점) -&amp;amp;gt; 실제 커넥션 (TCP, SocketTimeOut 발생지점)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNmoO/btsAxisrse0/9ZqAdcvtB2HuUNyEfA6ic0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNmoO%2FbtsAxisrse0%2F9ZqAdcvtB2HuUNyEfA6ic0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;737&quot; height=&quot;469&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Connection -&amp;gt; Statement (StatementTimeout 발생지점) -&amp;gt; 실제 커넥션 (TCP, SocketTimeOut 발생지점)&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;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 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;  DB Connection Pool ( DBCP) 는 왜 사용하나요?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwOsAO/btsAD48nGwU/Z90OjDx7ljk5j6qZ3aqYJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwOsAO/btsAD48nGwU/Z90OjDx7ljk5j6qZ3aqYJk/img.png&quot; data-alt=&quot;미리 커넥션(TCP 연결)을 다 해놓고 Connection 객체를 제공하는 시점에 DB HealthCheck 만 한번 해보고 재사용 하는 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwOsAO/btsAD48nGwU/Z90OjDx7ljk5j6qZ3aqYJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwOsAO%2FbtsAD48nGwU%2FZ90OjDx7ljk5j6qZ3aqYJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;315&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;미리 커넥션(TCP 연결)을 다 해놓고 Connection 객체를 제공하는 시점에 DB HealthCheck 만 한번 해보고 재사용 하는 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOwNpj/btsCAgFW2Yt/3NwvJxzVke42FRrJ7K4W81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOwNpj/btsCAgFW2Yt/3NwvJxzVke42FRrJ7K4W81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOwNpj/btsCAgFW2Yt/3NwvJxzVke42FRrJ7K4W81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOwNpj%2FbtsCAgFW2Yt%2F3NwvJxzVke42FRrJ7K4W81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1228&quot; height=&quot;438&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;DB 커넥션을 얻는 과정&lt;br /&gt;1. [ 어플리케이션 서버]는 JDBC 인터페이스를 이용해 실제 DB 드라이버를 사용하여 커넥션을 조회한다.&lt;br /&gt;2. 요청 받은 DB 드라이버는 TCP/IP 커넥션을 연결한다. (TCP 3 Way-Handshake)&lt;br /&gt;3. 연결된 커넥션으로 [DB 서버]에 로그인 요청 및 세션 부가정보를 전달한다.&lt;br /&gt;4. 요청받은 [DB 서버]는 인증을 완료하고 [어플리케이션 서버]와 통신할 DB 세션을 내부적으로 생성한다.&lt;br /&gt;5. [DB 서버]는 커넥션 생성이 완료되었다는 응답을 보낸다.&lt;br /&gt;6. DB 드라이버는 연결이 완료되었는지 검증한 후, 새로운 Connection 객체를 [어플리케이션 서버]에 전달한다.&amp;nbsp; &amp;nbsp;&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;DB Connection Pool (이하 DBCP) 를 연결하고 객체를 초기화하는 과정은 비용도 많이 들고 그 과정 자체도 복잡합니다.&amp;nbsp; 그래서 매번 새롭게 연결하지 않고 커넥션 풀(= 커넥션 저장소)를 이용해 미리 커넥션을 만들어두고 재사용하게됩니다. &lt;span style=&quot;color: #0593d3;&quot;&gt;물론 개발자가 직업 Map을 만들어서 관리해도 됩니다만 보통은 잘 만들어진 커넥션풀 라이브러리 를 이용합니다. 스프링은 기본으로 HikariCP 를 사용하는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://github.com/brettwooldridge/HikariCP-benchmark&quot;&gt;벤치마크 링크&lt;/a&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;972&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwOsAO/btsAD48nGwU/Z90OjDx7ljk5j6qZ3aqYJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwOsAO/btsAD48nGwU/Z90OjDx7ljk5j6qZ3aqYJk/img.png&quot; data-alt=&quot;미리 커넥션(TCP 연결)을 다 해놓고 Connection 객체를 제공하는 시점에 DB HealthCheck 만 한번 해보고 재사용 하는 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwOsAO/btsAD48nGwU/Z90OjDx7ljk5j6qZ3aqYJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwOsAO%2FbtsAD48nGwU%2FZ90OjDx7ljk5j6qZ3aqYJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;315&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;미리 커넥션(TCP 연결)을 다 해놓고 Connection 객체를 제공하는 시점에 DB HealthCheck 만 한번 해보고 재사용 하는 방식&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;Java 커넥션 풀 라이브러리의 경우 대부분 벤더사의 DataSource를 래핑하여 제공합니다. 예를 들어 HikariCP라면 HikariDataSource 에 사용할 DB를 설정하면 바로 사용가능하게 구성해둡니다. 이때 설정하는 방법은 2가지가 있는데 보통 jdbcUrl로 많이 연결합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lt; JDBC URL + DB Driver &amp;gt; 설정 &lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;jdbcUrl: jdbc:mysql://localhost:3306/world?serverTimeZone=UTC&amp;amp;CharacterEncoding=UTF-8&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt; DataSource 구현체 &amp;gt;&amp;nbsp; className  설정&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt; dataSourceClassName: oracle.jdbc.pool.OracleDataSource&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;span&gt;&amp;nbsp;&lt;/span&gt;DataSource 구현체 설정 (DataSource-ClassName 설정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSource 구현체를 직접 명시하여  커넥션 풀을 설정합니다. 특정 DataSource 구현체만 있는 설정을 쉽게 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xbjwG/btsAF0pL92y/41Lm2CKwjsk8y24UEJ6Mwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xbjwG/btsAF0pL92y/41Lm2CKwjsk8y24UEJ6Mwk/img.png&quot; data-alt=&quot;DataSource 이름은 여기에서 확인가능합니다. https://github.com/brettwooldridge/HikariCP#popular-datasource-class-names&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xbjwG/btsAF0pL92y/41Lm2CKwjsk8y24UEJ6Mwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxbjwG%2FbtsAF0pL92y%2F41Lm2CKwjsk8y24UEJ6Mwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1398&quot; height=&quot;472&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DataSource 이름은 여기에서 확인가능합니다. https://github.com/brettwooldridge/HikariCP#popular-datasource-class-names&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1700569380396&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// DataSourceClassName 설정은 com.zaxxer.hikari.util.DriverDataSource 기반으로 동작합니다.
HikariConfig config = new HikariConfig();

// PostgreSQL의 DataSource 구현체 클래스 이름을 설정
config.setDataSourceClassName(&quot;org.postgresql.ds.PGSimpleDataSource&quot;);
config.addDataSourceProperty(&quot;user&quot;, &quot;user&quot;);
config.addDataSourceProperty(&quot;password&quot;, &quot;password&quot;);
HikariDataSource dataSource = new HikariDataSource(config);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;자바 표준 JDBC URL + DB Driver (driver-class-name)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 jdbcUrl을 그대로 사용해서 설정합니다. 직관적이고 편해서 많이 사용하지만 특정 DB에만 있는 설정을 추가하기 어렵습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700569380398&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;HikariConfig config = new HikariConfig();

// PostgreSQL 데이터베이스에 연결하기 위한 JDBC URL 설정
String jdbcUrl = &quot;jdbc:postgresql://localhost:5432/mydatabase&quot;;
config.setJdbcUrl(jdbcUrl);
HikariDataSource dataSource = new HikariDataSource(config);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 보면 어색할수도 있는데 스프링부트의 자동구성을 사용한다면 아래와 같이 yml로 편하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;가끔 DB 자체 설정으로 착각하시는 경우가 있는데, 이는  HikariDataSource에서 하는 서버 타임아웃 값 &amp;amp; 커넥션 풀 설정입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1700569380398&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;spring:
 datasource:
   # ex) spring.datasource.driver-class-name &amp;rarr; HikariConfig.setDriverClassName()
   driver-class-name: com.mysql.cj.jdbc.Driver
   url: jdbc:mysql://localhost:3306/world?serverTimeZone=UTC&amp;amp;CharacterEncoding=UTF-8
   username: root
   password: your_password
   hikari:
      # 스레드가 HikariCP에 connection을 요청하고, 이를 대기하는 최대시간입니다. * DB timeout 아닙니다.
      connection-timeout: 3000
      # HikariCP 커넥션 풀의 이름.
      pool-name: HikariCP-Writer
      # 기본으로 설정할 autoCommit 값. 보통 read-only는 true 로, writer는 false로 설정합니다.
      auto-commit: false
      
      # JDBC 4.0 이전에는 각 DB가 Connection.isValid() 같은 걸 지원하지 않아서 유용하게 사용했다.
      # 물론 지금은 전~혀 필요 없는 기능
      connection-init-sql: SET TIME ZONE 'UTC' # 커넥션 생성할 때 마다 쿼리
      connection-test-query: SELECT 1 # 새로운 커넥션을 주기 전 DB 헬스체크 하는 쿼리
      
      # 새로운 커넥션을 주기 전 DB 헬스체크 timeout 시간, 참고로 connection-init-sql에도 같이 적용됨
      validation-timeout: 2000
      
      # 풀에서 유지되는 최소한의 유휴 커넥션 수.
      minimum-idle: 10
      # 풀에서 관리할 수 있는 최대 커넥션 수.
      maximum-pool-size: 50
      # 커넥션이 유휴 상태로 있을 수 있는 최대 시간(밀리초), DB 자체 커넥션 수가 부족하다면 시간을 줄여야함
      idle-timeout: 30000
      # 풀에 커넥션이 생성된 후 있을 수 있는 최대시간 (밀리초), DB 자체 커넥션 수가 부족하다면 시간을 줄여야함
      max-lifetime: 50000&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;참고로 reader, writer를 따로 사용한다면 아래와 같이 @ConfigurationProperties를  활용해 각각 따로 설정해주기도 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D3b8L/btsAzR1KMEh/kdWpm0vVwjoUDNBFKLgYaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D3b8L/btsAzR1KMEh/kdWpm0vVwjoUDNBFKLgYaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D3b8L/btsAzR1KMEh/kdWpm0vVwjoUDNBFKLgYaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD3b8L%2FbtsAzR1KMEh%2FkdWpm0vVwjoUDNBFKLgYaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2128&quot; height=&quot;1080&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;  커넥션 풀(HikariCP)  은 어떻게 설정하는게 좋을까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP 기준으로 설명하면 추천값은 아래와 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 641px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 17px;&quot;&gt;HikariCP 설정&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 17px;&quot;&gt;설명&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 17px;&quot;&gt;추천값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 17px;&quot;&gt;maximum-pool-size&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;커넥션 풀의 최대 크기&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 17px;&quot;&gt;(기본값 10개) 추천값 50 개&lt;br /&gt;상황에 따라 조절해야한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 17px;&quot;&gt;minimum-idle&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;커넥션&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;풀의 최소 크기&lt;br /&gt;정확히는 풀에서 유지될 최소 유후(Idle) 개수&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 17px;&quot;&gt;(기본값 10개) 설정하지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 75px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 75px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 57.9651%; height: 75px;&quot; colspan=&quot;2&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;✅ 응답속도가 중요한 시스템이라면 max 와 min 을 동일하게 맞춰 항상 커넥션이 살아있도록 유지한다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;✅ pool size의 경우 서버의 최대 스레드 수를 적절하게 지정해야한다. 커넥션 또한 Pool Locking 상태가 되어 데드락이 발생할 수 있다. &lt;span style=&quot;color: #0593d3;&quot;&gt;*글 하단에 추가 설명있음&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 43px;&quot;&gt;connection-timeout&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 43px;&quot;&gt;HikariCP 커넥션 요청 후 대기하는 시간 (DB 설정과는 관련 없다)&lt;br /&gt;&lt;br /&gt;즉, 이는 다른 모든 대기시간을 포함하기에 statement-timeout 과 validation-timeout을 고려해서 지정해야한다.&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 43px;&quot;&gt;(기본값 30초) 추천값 3,000ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 136px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 136px;&quot;&gt;max-lifetime&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 136px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;커넥션 풀에서 대기할 수 있는 커넥션의 최대 생명주기(시간)&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 136px;&quot;&gt;(기본값 30분) 추천값 50,000ms&lt;br /&gt;&lt;br /&gt;DB의 wait_timeout (idle_transaction_session_timeout) 보다 반드시 작게 설정해야 의미가 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 75px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 75px;&quot;&gt;idle-timeout&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 75px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;놀고있는 (idle) 커넥션이 자동 Close 되기 전 대기시간&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 75px;&quot;&gt;추천값 50,000ms&lt;br /&gt;pool size 를 고정해서 사용한다면 0 (무한)으로 설정한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 93px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 93px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 57.9651%; height: 93px;&quot; colspan=&quot;2&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;✅ max-lifetime 과 idle-timeout을 넘기더라도 minimum-idle 를 초과한 경우만 커넥션이 제거됩니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;✅ idle-timeout은 max-lifetime보다 작은 값이어야 의미있습니다. 아니면 idle 상태가 되기 전 커넥션이 삭제되었다가 새로 생성될테니까요.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 41px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 41px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;keepalive-time&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 41px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;DB 가 커넥션을 자체적으로 끊지 않도록 HikariCP가 DB 헬스체크 Connection.isValid() 를 호출해주는 최대시간입니다.&lt;br /&gt;&lt;br /&gt;참고로 30,000ms 보다 작게 설정할 수 없게 막혀있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 41px;&quot;&gt;(기본값 60000ms) 기본값 사용&lt;br /&gt;응답 속도가 중요하다면 30,000ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;validation-timeout&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;HikariCP 에서 커넥션을 주기 전 DB 헬스체크 의 타임아웃, Connection.isValid()&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 36px;&quot;&gt;추천값 1000ms 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 57px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 57.9651%; height: 57px;&quot; colspan=&quot;2&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;✅ DB 헬스체크 용도인 Connection.isValid() 는 JDBC4.0 부터 지원하는 기능, 그 이전에는 connection-init-sql 같은 걸로 사용했는데 구시대 유물이라 사용할 일이 없다.&lt;span style=&quot;color: #0593d3;&quot;&gt; 참고로 connection-init-sql에도 validation-timeout이 동일하게 걸린다.&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;autoCommit&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;위에서도 설명한 auto-commit의 기본값&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 17px;&quot;&gt;Writer 작업이 많은경우 true&lt;br /&gt;Read 작업이 많은 경우 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.0349%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.9883%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.9768%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 와 TCP 통신 중 발생하는 Connection Timeout 이나 Socket Timeout (==Read Timeout) 은 커넥션 풀과는 관련 없습니다. 필요하다면 이는 JDBC URL로 직접 설정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700571738932&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 기본값 설정, 특정 쿼리만 설정하고 싶다면 java.sql.Statement.setQueryTimeout(int timeout) 사용
spring:
  datasource:
    url: jdbc:mysql://hostname:3306/database?socketTimeout=10000&amp;amp;connectTimeout=10000&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;751&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCz2j6/btsAy9ImzUO/ddw0ZAZqv7WRtrbFMua4t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCz2j6/btsAy9ImzUO/ddw0ZAZqv7WRtrbFMua4t1/img.png&quot; data-alt=&quot;HikariCP의 connection-timeout 과 JDBC Url에 거는 TCP 통신 자체의 connection-timeout은 완전 다른 설정임을 유의하자.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCz2j6/btsAy9ImzUO/ddw0ZAZqv7WRtrbFMua4t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCz2j6%2FbtsAy9ImzUO%2Fddw0ZAZqv7WRtrbFMua4t1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;751&quot; height=&quot;271&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HikariCP의 connection-timeout 과 JDBC Url에 거는 TCP 통신 자체의 connection-timeout은 완전 다른 설정임을 유의하자.&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;JDBC에서 따로 제공하는 Statement Timeout은 Statement.setQueryTimeout() 메서드로 직접 걸 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oBqOf/btsAF7bs4SJ/vtxvt5DFFCc0o7zNvdp8ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oBqOf/btsAF7bs4SJ/vtxvt5DFFCc0o7zNvdp8ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oBqOf/btsAF7bs4SJ/vtxvt5DFFCc0o7zNvdp8ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoBqOf%2FbtsAF7bs4SJ%2Fvtxvt5DFFCc0o7zNvdp8ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;737&quot; height=&quot;469&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 Spring @Transaction(timeout=...)을 사용한다면, 스프링에서 제공하는 Transaction Timeout도 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AYfWL/btsABAk5tr3/clj44jlr05oaVTPEGwPoT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AYfWL/btsABAk5tr3/clj44jlr05oaVTPEGwPoT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AYfWL/btsABAk5tr3/clj44jlr05oaVTPEGwPoT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAYfWL%2FbtsABAk5tr3%2Fclj44jlr05oaVTPEGwPoT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;571&quot; height=&quot;360&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oBqOf/btsAF7bs4SJ/vtxvt5DFFCc0o7zNvdp8ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oBqOf/btsAF7bs4SJ/vtxvt5DFFCc0o7zNvdp8ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oBqOf/btsAF7bs4SJ/vtxvt5DFFCc0o7zNvdp8ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoBqOf%2FbtsAF7bs4SJ%2Fvtxvt5DFFCc0o7zNvdp8ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;737&quot; height=&quot;469&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Connection Pool 과 스레드의 상관관계 ( HikariCP 데드락 )&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;connection pool size의 경우 서버의 최대 스레드 수를 적절하게 지정해주어야 합니다. 아니면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Pool Locking 으로 HikariCP 자체 데드락이 발생&lt;/b&gt;할 수 있습니다. 다행히도 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot;&gt;잘 알려진 이슈&lt;/a&gt;라서 아래 공식에 따라 개수를 지정해주면 안전합니다.&lt;br /&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;1782&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co8jeJ/btsAD4zsNZC/MlKFLcNQWywX8jk4BzqJak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co8jeJ/btsAD4zsNZC/MlKFLcNQWywX8jk4BzqJak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co8jeJ/btsAD4zsNZC/MlKFLcNQWywX8jk4BzqJak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco8jeJ%2FbtsAD4zsNZC%2FMlKFLcNQWywX8jk4BzqJak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1782&quot; height=&quot;448&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;Pool Locking은 아래와 같은 상황에서 발생할 수 있습니다.&lt;br /&gt;1. 현재 한 트랜잭션 (하나의 스레드)에서 커넥션을 물고 있는 상태로, 커넥션을 하나 더 사용하려고 시도함&lt;br /&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;예를 들어 insert 할 때 id_auto_increment를 사용하지 않는 경우 서브 트랜잭션으로 ID 받아오는 커넥션 추가 생성&lt;/span&gt;&lt;br /&gt;2. 그런데 서버를 빡세게 돌려서 풀에 남아 있는 커넥션이 없음.&lt;br /&gt;3. 모든 스레드가 커넥션을 1개씩 들고 서로가 서로를 기다리는 데드락 완성 ^ㅁ^&lt;br /&gt;4. 이제 커넥션을 얻지 못해서, HikariCP 기본 timeout인 30초를 넘겨 대부분의 요청이 rollback -&amp;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;color: #333333; text-align: start;&quot;&gt;해당 현상에 대해서는 &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://techblog.woowahan.com/2664/&quot;&gt;(2020 배달의민족) HikariCP&amp;nbsp;Dead&amp;nbsp;lock에서&amp;nbsp;벗어나기&amp;nbsp;(이론편)&lt;/a&gt; &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;글에서도 자세하게 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;  레퍼런스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://netmarble.engineering/jdbc-timeout-for-game-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(2022 넷마블 엔지니어링) 게임 서버 시스템을 위한 JDBC와 Timeout 이해하기&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://netmarble.engineering/hikaricp-options-optimization-for-game-server/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(2022 넷마블 엔지니어링) 게임&amp;nbsp;서버&amp;nbsp;시스템을&amp;nbsp;위한&amp;nbsp;HikariCP&amp;nbsp;옵션&amp;nbsp;및&amp;nbsp;권장&amp;nbsp;설정&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1700572604778&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;게임 서버 시스템을 위한 HikariCP 옵션 및 권장 설정 - 넷마블 기술 블로그&quot; data-og-description=&quot;HikariCP는 특별히 옵션을 튜닝하지 않더라도 대부분의 개발 및 배포에서 충분한 성능으로 동작합니다. 하지만 게임 서버 시스템을 위한 JDBC와 Timeout 이해하기에서 이야기한 것처럼 WAS 시스템을 &quot; data-og-host=&quot;netmarble.engineering&quot; data-og-source-url=&quot;https://netmarble.engineering/hikaricp-options-optimization-for-game-server/&quot; data-og-url=&quot;https://netmarble.engineering/hikaricp-options-optimization-for-game-server/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/upTIb/hyUCaMGSod/0eD8I7xRkmJDOb08qmLlT1/img.png?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270&quot;&gt;&lt;a href=&quot;https://netmarble.engineering/hikaricp-options-optimization-for-game-server/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://netmarble.engineering/hikaricp-options-optimization-for-game-server/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/upTIb/hyUCaMGSod/0eD8I7xRkmJDOb08qmLlT1/img.png?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;게임 서버 시스템을 위한 HikariCP 옵션 및 권장 설정 - 넷마블 기술 블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;HikariCP는 특별히 옵션을 튜닝하지 않더라도 대부분의 개발 및 배포에서 충분한 성능으로 동작합니다. 하지만 게임 서버 시스템을 위한 JDBC와 Timeout 이해하기에서 이야기한 것처럼 WAS 시스템을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;netmarble.engineering&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://techblog.woowahan.com/2664/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(2020 배달의민족) HikariCP&amp;nbsp;Dead&amp;nbsp;lock에서&amp;nbsp;벗어나기&amp;nbsp;(이론편)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://woowabros.github.io/experience/2020/02/06/hikaricp-avoid-dead-lock-2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(2020 배달의민족) HikariCP Dead lock에서 벗어나기 (실전편)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/5102792&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(2015 네이버 D2) Commons DBCP 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  2025/JDBC &amp;amp; JPA</category>
      <author>JiwonDev</author>
      <guid isPermaLink="true">https://jiwondev.tistory.com/291</guid>
      <comments>https://jiwondev.tistory.com/291#entry291comment</comments>
      <pubDate>Tue, 21 Nov 2023 22:30:00 +0900</pubDate>
    </item>
  </channel>
</rss>