<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Ray`s Tech Lab</title>
    <link>https://jofestudio.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 21 Jul 2026 12:33:00 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Ray123</managingEditor>
    <item>
      <title>[결제 개발기] (1) PG는 실제 결제를 어떻게 다룰까? 인증결제 흐름 직접 설계하기</title>
      <link>https://jofestudio.tistory.com/157</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;들어가며&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 예전부터 '결제'에 대한 궁금증이 있었습니다. 돈을 다루는 만큼 정합성을 아주 중요히 봐야 하지만, 성능도 함께 챙겨야 하기 때문에 엔지니어가 여러 고민을 하고 다양한 시도를 해볼 수 있는 도메인이라고 생각했었거든요. 단순한 결제 연동을 넘어, 결제라는 도메인에서 발생하는 복잡한 데이터 흐름과 어려움들을 직접 경험해보고 다뤄보고 싶다는 마음이 생겼었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 결제를 처리할 때 PG사 내부에서 어떤 일들이 벌어지고 있는지 또 어떤 고민들이 일어나고 있는지 알아보고 싶었습니다. 그래서 요즘은 퇴근 후 토이 성격의 PG를 만들면서 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;가맹점 서버가 아닌 PG사 입장에서 결제 준비 ~ 카드사 승인 등의 흐름을 직접 공부하고 설계&lt;/b&gt;&lt;/span&gt;하고 있는데요, 그 첫 번째 기록으로 이 글을 쓰고 있습니다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;글 시작 전 2가지를 명확히 하자면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이 프로젝트는 카드결제를 기준으로 하고, 또 토이 프로젝트라는 점&lt;/b&gt;&lt;/span&gt;입니다. 토스페이먼츠, 나이스페이먼츠 등 여러 PG 사의 docs와 테스트 환경을 실제로 분석해보면서 PG사 관점의 결제 흐름을 최대한 모사하려 했지만, PG사와 카드사, VAN 등의 내부 통신을 정확히 알 수는 없었기 때문에 결국 &quot;이렇게 되고 있을 것이다&quot;라고 가정하고 처리하는 부분들이 있습니다. 따라서 이 프로젝트에서 사용한 결제 흐름이 실제와는 차이가 있을 수 있습니다만, 최대한 분석하며 실제에 가깝게 만들고자 했습니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;목차&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 결제 도메인 기본 이해&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. '인증결제'의 흐름 살펴보기&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. mini-pg 설계&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 결제 도메인 기본 이해&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;결제 주요 참여자&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 PG사들의 docs들을 여러 번 읽으며 결제 흐름과 관련 용어들을 공부해봤는데요. 결제에 관여하는 주요 요소들은 다음처럼 정리해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1720&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/comToO/dJMcaiX0tVO/b1eaGszjaQcAUTFdeoq4J0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/comToO/dJMcaiX0tVO/b1eaGszjaQcAUTFdeoq4J0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/comToO/dJMcaiX0tVO/b1eaGszjaQcAUTFdeoq4J0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcomToO%2FdJMcaiX0tVO%2Fb1eaGszjaQcAUTFdeoq4J0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;896&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 고객&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;카드사로부터 카드를 발급받은 고객을 말합니다&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 가맹점&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물건을 파는 사업자로, 결제 인프라를 직접 만들지 않고 PG사에 가입하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. PG사&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Payment Gateway의 줄임말로, 카드사들을 한 데 모아 가맹점에 제공하는 회사의 역할을 합니다. 고객들이 보는 결제창, 결제 API, 정산 등을 책임집니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;이&lt;/b&gt; &lt;b&gt;토이 프로젝트(mini-pg)에 해당하는 요소&lt;/b&gt;&lt;/span&gt;입니다!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. VAN&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Value Added Network의 줄임말로, PG사와 카드사 사이, 또는 오프라인 단말기와 카드사 사이에서 거래 데이터를 중계하는 통신망 중계 사업자입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;5. 카드사&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;두 종류가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;발급사 : 실물 카드를 발급하고 회원을 관리하는 곳입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;매입사 : 카드 거래를 처리하고 가맹점에 대금을 입금해 주는 곳입니다. 참고로 국내엔 총 9개(국민, 삼성, 신한, 현대, 하나, 비씨, 롯데, 농협, 우리)가 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;6. 은행 + 금융결제원&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제 원화 자금이 오가는 인프라로, 카드사가 가맹점에 대금을 지급할 때 또는 PG사가 가맹점에 정산금을 보낼 때 등이 모두 은행 계좌이체로 이뤄지고 그 중계가 금융결제원 망을 통해 수행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;mini-pg 에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;가맹점과 PG, 카드사만 모델링&lt;/b&gt;&lt;/span&gt;했습니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;결제 기본 흐름&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결제의 본질은 결국 고객과 가맹점 사이에서 직접 돈이 오가는게 아니라, 믿을 수 있는 제 3자(PG사, 은행, 카드사)가 고객의 돈을 가맹점으로 대신 전달해주는 것으로 볼 수 있습니다. 이 관점에서 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결제는 크게 다음 4가지 순서로 진행된다고 이해할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;1662&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzznRp/dJMcaff1WzW/sJdGRGQ6RHTGzsG3pXvAn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzznRp/dJMcaff1WzW/sJdGRGQ6RHTGzsG3pXvAn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzznRp/dJMcaff1WzW/sJdGRGQ6RHTGzsG3pXvAn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzznRp%2FdJMcaff1WzW%2FsJdGRGQ6RHTGzsG3pXvAn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1662&quot; height=&quot;404&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 인증&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;ldquo;지금 이 사람이 이 카드의 주인이 진짜 맞는지&amp;rdquo;를 보는 단계입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 승인&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;카드가 유효한지 확인하고 돈을 잡아두는 단계로 신용카드 결제는 이 단계에서 실제 돈이 나가지 않고 홀드만 하는 반면(한도만 차감한다고 표현), 체크카드는 이 단계에서 돈이 나가게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 매입&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PG사가 승인된 거래들을 모아서 카드사로 대금 지급을 청구하는 단계로, 카드사는 매입이 완료되면 PG사로 돈을 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 정산&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;매입이 완료되어 PG사에 온 돈을 수수료를 떼고 가맹점으로 주는 단계를 말합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;mini-pg 에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;승인&lt;/b&gt;&lt;/span&gt;에 초점을 맞춰 다뤄보기로 했습니다&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 결제의 흐름 쪽에서 어떤 데이터들이 오고 가는지를 좀 더 중점적으로 살펴봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. '인증결제'의 흐름 살펴보기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;아래 내용은 분석 과정에서 제가 가정한 결제 흐름들을 포함합니다. PG의 실제 결제 흐름과는 다를 수 있습니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결제 흐름 모사를 위해 토스페이먼츠, 나이스 페이먼츠 등 각 PG사에서 퍼블릭하게 제공하는 연동 가이드들을 정말 수차례 살펴봤습니다. 해당 가이드들에서는 인증결제(고객이 결제수단의 소유자라는 본인인증을 마치고 결제하는 것)를 위주로 다루고 있는데요. 결제 도메인이 처음인 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제 입장에서 특히 혼란스러웠던 점은 아래처럼 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;가맹점이 PG의 결제승인 API를 호출할 때의 payload에는 카드번호가 없는데 응답에는 카드번호가 있다는 점&lt;/b&gt;&lt;/span&gt;이었습니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1) 토스페이먼츠 결제승인 API 요청 &amp;amp; 응답 예제&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783859062525&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl --request POST \
  --url https://api.tosspayments.com/v1/payments/confirm \
  --header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5Bc...' \
  --header 'Content-Type: application/json' \
  --data '{
    &quot;paymentKey&quot;:&quot;5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1&quot;,
    &quot;orderId&quot;:&quot;a4CWyWY5m89PNh7xJwhk1&quot;,
    &quot;amount&quot;:1000
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783859436978&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;mId&quot;: &quot;tosspayments&quot;,
    &quot;paymentKey&quot;: &quot;5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1&quot;,
    &quot;orderId&quot;: &quot;a4CWyWY5m89PNh7xJwhk1&quot;,
    &quot;card&quot;: {
        &quot;issuerCode&quot;: &quot;71&quot;,
        &quot;acquirerCode&quot;: &quot;71&quot;,
        &quot;number&quot;: &quot;12345678****000*&quot;,
        &quot;cardType&quot;: &quot;신용&quot;,
        &quot;ownerType&quot;: &quot;개인&quot;,
        , ...생략
    },
    , ...생략 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 나이스페이먼츠 결제승인 API 요청 &amp;amp; 응답 예제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783859097934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl --request POST \
  --url https://sandbox-api.nicepay.co.kr/v1/payments/UT0000113m01012111051714341073 \
  --header 'Authorization: Basic UzJfYWY0NTQz...' \
  --header 'Content-Type: application/json' \
  --data '{
    &quot;amount&quot;: 1004
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783859341073&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;tid&quot;: &quot;UT0000113m01012111051714341073&quot;,
    &quot;card&quot;: {
        &quot;cardCode&quot;: &quot;04&quot;,
        &quot;cardName&quot;: &quot;삼성&quot;,
        &quot;cardNum&quot;: &quot;12341234****1234&quot;,
        , ... 생략
    },
    , ... 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;&quot;PG의 결제 승인 API를 호출할 땐 카드번호를 넘기지 않는데, 어떻게 사용자가 선택한 카드로 승인이 일어나고 있으며 어떻게 응답 내용에는 마스킹된 카드번호가 담겨 있을까?&quot;&lt;/i&gt; 라는 의문이 자연스럽게 생길 수밖에 없었습니다. PG사들이 제공하는 가이드 문서로는 결제 승인 API의 형식 정도만 말해주고 있기 때문에, 인증 단계에서 무슨 일이 벌어지는지, 카드번호가 어떻게 전달되는지 등은 직접 분석하면서 가정할 수밖에 없었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;&quot;어떻게 되고 있는 건지는 모르겠다. 어차피 토이인데 비인증 결제만 다룬다 치고, 우리는 카드번호를 payload에 담는 결제 승인 API만 만드는 걸로 하자!&quot; &lt;/i&gt;라고 할 수도 있었지만, 실제에 가까운 프로젝트를 모사 + 결제 도메인 공부도 해볼 겸 인증결제는 어떻게 진행되고 있는지 좀 더 뜯어봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개인적으론 &lt;i&gt;&quot;PG는 사용자나 가맹점으로부터 결제에 쓰일 카드번호를 받고 있고, PG사가 카드사로 승인요청을 보낼때 그 카드번호를 활용한다&quot;&lt;/i&gt; 라는 가정을 하고 있었던 터라 여러 시행착오를 겪었는데요. 결론적으로 어찌저찌 알아낸 점들은 다음과 같았습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(PG사별로 테스트 환경을 제공해주는게 너무 고마웠습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;1. 인증결제는 사용자 or 가맹점에서 PG로 카드번호를 보내지 않으며, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;인증은 사용자와 카드사끼리 즉 PG 밖에서 발생&lt;/b&gt;&lt;/span&gt; (관련있는 보안 표준으로 pci-dss가 있습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 즉 'PG &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: center;&quot;&gt;&amp;rarr;&lt;/span&gt; 카드사로의 승인요청'은 카드번호를 기준으로 라우팅하는게 아니며, 실제 결제에 사용될 카드번호는 카드사가 안다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 결제창을 연 뒤로는 브라우저에서 PG로 요청을 보내면서 '이 결제의 인증완료 여부'를 체크하는 과정이 있음&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 즉 사용자와 카드사 사이에서 일어나는 인증이 완료되면 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;PG도 인증이 끝났음을 알게 되는 과정&lt;/b&gt;&lt;/span&gt;이 있다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. 인증완료가 확인되면 리다이렉트 등을 수행하고, 그 후 가맹점은 PG로 결제승인 API를 호출하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 토스페이먼츠의 테스트 환경에서 개발자 도구의 네트워크 탭을 켜고 모든 HTTP 요청들을 수집해서 분석했었는데, 토스페이먼츠는 인증 과정에서&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 결제창에서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;사&lt;/b&gt;&lt;b&gt;용자가 선택한 수단(카드사 포함)을 토스페이먼츠로 보내는 과정&lt;/b&gt;&lt;/span&gt;이 있음&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;- 결제금액, 카드사정보 등을 담아 보내는 결제 파라미터 관련 호출을 하는 과정이 있었습니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 카드사별 인증창이 닫히면, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;인증 완료를 토스페이먼츠에 알리는 과정&lt;/b&gt;&lt;/span&gt;이 있음&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- 카드사 인증창이 닫힌 직후, 브라우저가 카드사 쪽 API로 인증 결과를 요청한 뒤 이어서 토스페이먼츠로 인증 관련 호출을 하는 과정이 있었습니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 2번 과정이 정상적으로 끝나면 결제성공 url로 이동&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;를 하고 있었습니다. 물론 PG사별 결제 진행 방식엔 서로 차이가 있겠습니다만, 토스페이먼츠의 결제 흐름 분석을 바탕으로 저는 아래처럼 결제 흐름을 가정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 사용자와 카드사 간 인증이 일어날 때, PG는 사용자가 어느 카드사를 선택했었는지 알게 된다 (= PG로 사용자가 고른 카드사 등을 전달하는 과정이 있다).&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;1474&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgDq5b/dJMcaiRzRZo/3jJFK1fi9ld2VZaknR4OFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgDq5b/dJMcaiRzRZo/3jJFK1fi9ld2VZaknR4OFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgDq5b/dJMcaiRzRZo/3jJFK1fi9ld2VZaknR4OFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgDq5b%2FdJMcaiRzRZo%2F3jJFK1fi9ld2VZaknR4OFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1474&quot; height=&quot;420&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 또한 사용자와 카드사 간 인증이 일어날 때, 카드사는 어떠한 결제식별자에 어떤 카드가 사용되는지에 대한 정보를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAeK4h/dJMcabESoJp/cCi3YLM7tY9eaYRo9yqmWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAeK4h/dJMcabESoJp/cCi3YLM7tY9eaYRo9yqmWk/img.png&quot; data-alt=&quot;편의를 위해 카드사 내의 tid와 PG가 다루는 paymentId는 같다고 가정했습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAeK4h/dJMcabESoJp/cCi3YLM7tY9eaYRo9yqmWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAeK4h%2FdJMcabESoJp%2FcCi3YLM7tY9eaYRo9yqmWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1472&quot; height=&quot;568&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;편의를 위해 카드사 내의 tid와 PG가 다루는 paymentId는 같다고 가정했습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 또한 사용자와 카드사 간 인증이 끝나면, 카드사에서 인증값(1회용 인증토큰)을 만들어주고 이 값은 PG에 저장된다.&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;1594&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/swvIE/dJMcadJo7XC/YpYdZj2se7EKAIeuJNGeY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/swvIE/dJMcadJo7XC/YpYdZj2se7EKAIeuJNGeY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/swvIE/dJMcadJo7XC/YpYdZj2se7EKAIeuJNGeY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FswvIE%2FdJMcadJo7XC%2FYpYdZj2se7EKAIeuJNGeY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1594&quot; height=&quot;478&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 이후 가맹점 서버가 PG로 결제승인 API를 호출하면, PG는 1번에서 받은 카드사 정보를 통해 특정 카드사로 승인요청을 보내며, 이때 3번에서 받은 인증값을 사용한다.&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;1582&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdnQzM/dJMcahrEawl/L38o4gtHZdLICo9shSCET1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdnQzM/dJMcahrEawl/L38o4gtHZdLICo9shSCET1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdnQzM/dJMcahrEawl/L38o4gtHZdLICo9shSCET1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdnQzM%2FdJMcahrEawl%2FL38o4gtHZdLICo9shSCET1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1582&quot; height=&quot;486&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. 카드사는 PG로부터 승인요청을 받으면, 인증토큰과 2번에서 저장했던 결제식별자를 활용해 사용자가 골랐던 카드로 승인을 해주고, 응답으로 마스킹된 카드번호를 PG에게 준다.&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;1514&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFBTqa/dJMcage8XRc/NuhIEVlrAMCVzTmDcYCnU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFBTqa/dJMcage8XRc/NuhIEVlrAMCVzTmDcYCnU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFBTqa/dJMcage8XRc/NuhIEVlrAMCVzTmDcYCnU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFBTqa%2FdJMcage8XRc%2FNuhIEVlrAMCVzTmDcYCnU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1514&quot; height=&quot;592&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 내용들을 바탕으로, 제가 가졌던 의문인 &lt;i&gt;&quot;PG의 결제 승인 API를 호출할 땐 카드번호를 넘기지 않는데, 어떻게 사용자가 선택한 카드로 승인이 일어나지?&quot; &lt;/i&gt;에 대해 답을 해보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 인증 결제에서는 PG는 사용자/가맹점으로부터 카드번호를 받지 않음&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. PG에서 어느 카드사로 라우팅해야 하는지는 인증 전에 확정됨&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. '결제 승인'은 인증토큰을 소모하는 행위임&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. mini-pg 설계&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위에서 가정한 흐름들을 바탕으로, 제가 하고 있는 토이 PG인 mini-pg를 아래처럼 설계했습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;3개 API를 정의했습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.39536%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Method&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.093%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.5116%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;설명&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.39536%; text-align: center;&quot;&gt;POST&lt;/td&gt;
&lt;td style=&quot;width: 27.093%;&quot;&gt;/api/payments/prepare&lt;/td&gt;
&lt;td style=&quot;width: 41.5116%;&quot;&gt;[ 결제준비 API ]&lt;br /&gt;&lt;br /&gt;- 요청 payload로 사용자가 선택한 카드사, 결제 금액, 수단 등을 받음&lt;br /&gt;- 호출되면 PG는 READY 상태로 결제 데이터를 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.39536%; text-align: center;&quot;&gt;POST&lt;/td&gt;
&lt;td style=&quot;width: 27.093%;&quot;&gt;/api/payments/{paymentId}/auth-complete&lt;/td&gt;
&lt;td style=&quot;width: 41.5116%;&quot;&gt;[ 인증완료 API ]&lt;br /&gt;&lt;br /&gt;- 호출되면 PG는 카드사로부터 해당 결제의 인증토큰을 받아옴&lt;br /&gt;- 인증토큰을 정상적으로 받아오면 결제 상태를 IN_PROGRESS로 갱신&lt;br /&gt;- 사용자 브라우저에서 카드사 인증창이 닫혀서 인증완료를 감지하면 브라우저에서 PG로 호출한다고 가정하는 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.39536%; text-align: center;&quot;&gt;POST&lt;/td&gt;
&lt;td style=&quot;width: 27.093%;&quot;&gt;/api/payments/approve&lt;/td&gt;
&lt;td style=&quot;width: 41.5116%;&quot;&gt;[ 결제승인 API ]&lt;br /&gt;&lt;br /&gt;- 요청 payload로 결제식별자, 결제 금액, 수단 등을 받음&lt;br /&gt;- 호출되면 PG는 결제 금액, 수단 등이 결제준비 API가 호출될 때 입력한 값과 동일한지 대조&lt;br /&gt;- 그 후 PG가 카드사로 해당 결제의 승인을 요청 (인증토큰을 담아 요청)&lt;br /&gt;- 카드사에서 승인 응답을 받으면 결제 상태를 APPROVED로 갱신&lt;br /&gt;- 카드사에서 실패 응답을 받으면 결제 상태를 ABORTED로 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;API별 요청 포맷, 응답 포맷은 생략합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;상태 변화&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결제 상태의 변화는 아래처럼 약소화하여 정의했습니다. (mini-pg가 승인 위주로 다루기 때문입니다)&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;748&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bklRrL/dJMcaiD3WlH/6HU7KqGcBMmMznMINKK8MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bklRrL/dJMcaiD3WlH/6HU7KqGcBMmMznMINKK8MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bklRrL/dJMcaiD3WlH/6HU7KqGcBMmMznMINKK8MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbklRrL%2FdJMcaiD3WlH%2F6HU7KqGcBMmMznMINKK8MK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;418&quot; height=&quot;604&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 지금의 범위에서 EXPIRED, CANCELED 전이까지 구현하진 않았고, 해당 상태로의 전이들은 추후 mini-pg를 확장해나가며 구현할 예정입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ERD&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ERD는 아래처럼 설계했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GCU3a/dJMcaixhf2e/2Ukj9XOdF69iMW4QxyZOw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GCU3a/dJMcaixhf2e/2Ukj9XOdF69iMW4QxyZOw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GCU3a/dJMcaixhf2e/2Ukj9XOdF69iMW4QxyZOw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGCU3a%2FdJMcaixhf2e%2F2Ukj9XOdF69iMW4QxyZOw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;762&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; &lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;span&gt; 각 컬럼별 상세 설명은 생략합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PAYMENT의 가맹점ID, 주문번호, 카드사, 총 결제금액 등은 결제준비 API가 호출될 때 등록되도록 했고, 이후 결제승인 API가 호출되면 이 값들과 대조하도록 했습니다. PAYMENT_AUTHENTICATION은 인증완료 API가 호출되면 PG가 카드사로부터 받아오는 인증토큰과 만료시간 등이 담기도록 했고, PAYMENT_CARD는 카드사가 승인 응답으로 준 값들을 저장하게 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;결제 흐름 상세 시퀀스&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시퀀스는 아래와 같이 설계했으며, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;제가 가정한 결제 흐름을 기반으로 설계&lt;/b&gt;&lt;/span&gt;했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1124&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/An22E/dJMcacRhHQq/gPzi3BBZrI082jQKfy2kS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/An22E/dJMcacRhHQq/gPzi3BBZrI082jQKfy2kS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/An22E/dJMcacRhHQq/gPzi3BBZrI082jQKfy2kS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAn22E%2FdJMcacRhHQq%2FgPzi3BBZrI082jQKfy2kS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;738&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 &quot;사용자가 결제창에서 인증을 하는 것&quot;까지는 토이 범위를 넘어선다고 판단해서 만들지는 않았습니다. 다만 토큰 발급과 카드사 승인 요청 정도만 간단하게 처리해줄 수 있는 토이 카드사까지는 간단하게 만들어서 mini-pg에 활용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;앞으로 할 것들&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음에 말한 것처럼 이 프로젝트를 하게 된 계기는 회사에서 하고 있는 것과 다른 영역의 도메인을 느끼고 싶은 것도 있으나, 기술적으로 여러 요소를 고민해보는 경험을 해보고 싶은 것도 있습니다. 대표적으로 각 API들을 일부러 멱등하게 구현하지 않았는데, 이런 상황에서 멱등성 보장을 위한 여러 방법들을 직접 해보면서(물론 ai의 도움과 함께) 익히고 싶은 목적이 있습니다. 그 외에도 장애 시나리오들을 가정하고 서킷 브레이커 등을 적용해보는 것, 비동기 이벤트를 발행하고 다루기, 전부터 관심있던 분산 트레이싱을 적용 등을 이 mini-pg를 통해 시간을 내보며 해보려 합니다.&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;참고한 자료들&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.tosspayments.com/resources/glossary/index&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.tosspayments.com/resources/glossary/index&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.tosspayments.com/reference&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.tosspayments.com/reference&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://start.nicepay.co.kr/manual/quickguide/start.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://start.nicepay.co.kr/manual/quickguide/start.do&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://developers.kakaopay.com/docs/payment/online/single-payment&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.kakaopay.com/docs/payment/online/single-payment&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.pay.naver.com/docs/onetime-payment/payment/apply&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.pay.naver.com/docs/onetime-payment/payment/apply&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://developers.hectofinancial.co.kr/docs/api/pg/getting-started/01-full-flow&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.hectofinancial.co.kr/docs/api/pg/getting-started/01-full-flow&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/결제도메인 개발</category>
      <category>PG</category>
      <category>개발</category>
      <category>결제</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/157</guid>
      <comments>https://jofestudio.tistory.com/157#entry157comment</comments>
      <pubDate>Tue, 14 Jul 2026 19:46:45 +0900</pubDate>
    </item>
    <item>
      <title>tmux가 어떤 도구인지 빠르고 쉽게 훑어보기</title>
      <link>https://jofestudio.tistory.com/156</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요즘 클로드 코드 등의 ai 도구를 다루게 되면서 tmux라는 걸 이용하면 터미널을 분할해서 작업할 수 있다~ 류의 이야기를 종종 듣게 된 것 같습니다. 그래서 tmux 사용법을 알아보던 중 세션, 윈도우같은 개념들이 나와 당황스럽기도 하고 tmux가 어떤 원리이길래 이런 게 가능한건지가 궁금해서 살펴봤었는데요. 찾아보니 tmux는 크게 두 가지를 해주는 도구였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;터미널 에뮬레이터을 닫아도 그 안에서 돌던 작업이 죽지 않게 해줍니다. 노트북을 덮었다가 다시 열어도, SSH가 끊겼다가 다시 붙어도 작업을 그대로 이어서 할 수 있게 해줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하나의 터미널을 여러 영역으로 분할해서 여러 작업을 동시에 볼 수 있게 해줍니다. 한쪽에선 서버를 띄우고, 다른 쪽에선 로그를 보고, 또 다른 쪽에선 코드를 편집하는 식으로 활용할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 두 가지가 어떻게 가능한지 살펴보던 과정에서 가장 중요했던 건 &quot;터미널 에뮬레이터와 쉘 프로세스가 어떤 관계인가&quot;였던 것 같습니다. 그래서 이 글에서는 그 관계부터 짚어보고, 그걸 tmux가 어떻게 바꿔놓는지, 그 위에서 패널, 윈도우, 세션이라는 단위들은 무엇을 의미하는지를 차례로 풀어보려고 합니다. 이 글을 통해 tmux가 어떤 원리의 도구인지, 그리고 왜 지금 다시 주목받고 있는지 이해할 수 있을 것으로 기대합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;※ tmux 사용법(단축키 등)은 이 글에서 다루지 않았습니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;※ 이 글에서는 터미널 에뮬레이터로 Ghostty를 사용한 것을 기준으로 작성했습니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;터미널 에뮬레이터&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 터미널 에뮬레이터의 개념부터 알아야 합니다. Ghostty, iTerm2 같은 앱들을 터미널 에뮬레이터라고 부르는데요,. 이 앱들은 화면을 렌더링하는 롤, 구체적으론 키보드 입력을 받아 쉘 프로세스에 전달하고 쉘 프로세스의 출력을 화면에 표시하는 역할을 맡습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 ls, cd, cat 같은 명령어를 해석하고 OS에 전달하는 건 쉘 프로세스입니다(애당초 쉘 프로세스 자체가 명령어를 해석하는 프로세스입니다). Ghostty를 켜는 순간 내부적으로 시스템에 설정된 기본 쉘(제 맥북 기준으론 zsh)를 자식 프로세스로 생성하며, 구조로 보면 이렇습니다. &lt;s&gt;&lt;i&gt;여담으로 운영체제 시간이었나? cat 등의 프로세스를 만들어보던 걸 실습해보던 기억이 났습니다&lt;/i&gt;&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819313121&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Ghostty (부모 프로세스)
  └── zsh (자식 프로세스)
        └── vim, python, ... (손자 프로세스)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 맥북에서 터미널 애뮬레이터로 Ghostty를 사용 중인데요, 실제로 pstree -p $$를 입력한 결과를 보면 다음처럼 Ghostty의 자식으로 zsh가 잡혀있는 것을 볼 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-03 오후 11.42.48.png&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FbaLV/dJMcaiwoQV4/AdrGxx1HNvNF580VuG7Ki1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FbaLV/dJMcaiwoQV4/AdrGxx1HNvNF580VuG7Ki1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FbaLV/dJMcaiwoQV4/AdrGxx1HNvNF580VuG7Ki1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFbaLV%2FdJMcaiwoQV4%2FAdrGxx1HNvNF580VuG7Ki1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2218&quot; height=&quot;452&quot; data-filename=&quot;스크린샷 2026-05-03 오후 11.42.48.png&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 통해 Ghostty를 끄게 되면 쉘 프로세스로 작업하던 내용들이 같이 종료되는 이유도 유추해볼 수 있습니다. 바로 SIGHUP(Signal Hang Up) 때문인데요, 부모 프로세스가 종료되면서 자식 프로세스들에게도 SIGHUP 신호가 전파되면서 함께 종료되는 것이 원인입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1777819660349&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Ghostty 종료
  &amp;darr; SIGHUP 전송
zsh 종료
  &amp;darr; SIGHUP 전송
vim, python, ... 종료&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;※ &lt;/span&gt;SSH로 원격 서버에 붙어 작업하다가 인터넷이 끊기면 작업이 날아가는 것도 같은 이유입니다. SSH 연결이 끊기는 순간 SIGHUP이 전파되어 실행 중이던 모든 프로세스가 종료됩니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;tmux는&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;만약 쉘 프로세스가 Ghostty가 아닌 다른 독립적인 프로세스의 자식 프로세스라면 Ghostty를 꺼도 해당 쉘은 살아 있으니 쉘에 다시 붙을 수 있겠다는 아이디어를 얻을 수 있는데요, tmux가 바로 이 점을 활용하는 도구입니다. Ghostty가 아닌 tmux라는 독립적인 프로세스의 자식으로 쉘 프로세스들을 만들어두고, 이 쉘 프로세스들에 붙게 만들어주는 식으로 동작하게 되는 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;tmux를 실행하면 다음과 같은 일이 일어납니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. tmux 서버 프로세스 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쉘에서 tmux 명령어 입력 시, tmux 서버 프로세스가 백그라운드에서&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;별도&lt;/b&gt;&lt;/span&gt;로 띄워집니다. (즉 쉘 프로세스의 자식으로 만들어지지 않습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. tmux 서버가 쉘 프로세스를 자식으로 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제 작업이 이루어지는 쉘 프로세스는 tmux 서버의 자식 프로세스로 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. tmux 클라이언트가 tmux 서버에 연결&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Ghostty 등의 터미널 애물레이터에서 보이는 것은 tmux 클라이언트로, tmux 서버와 소켓으로 연결되어 화면 출력만 주고받습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구조를 정리하면 이렇습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1777824199406&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;init (PID 1)
  └── tmux server          &amp;larr; Ghostty와 무관한 독립 프로세스
        └── zsh            &amp;larr; 실제 작업하는 쉘

Ghostty
  └── zsh
        └── tmux client    &amp;larr; 서버에 연결만 하는 클라이언트&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 tmux를 통해 접속하게 된 쉘에서 pstree -p $$를 하게 되면, Ghostty에서 pstree -p $$를 했을 때와는 다르게 쉘 프로세스의 부모가 Ghostty가 아니라 tmux로 잡히는 것을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-06 오후 10.41.11.png&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AaUQC/dJMcad2V5Bs/vHc2H0EJDX2YHf76ZzOuwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AaUQC/dJMcad2V5Bs/vHc2H0EJDX2YHf76ZzOuwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AaUQC/dJMcad2V5Bs/vHc2H0EJDX2YHf76ZzOuwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAaUQC%2FdJMcad2V5Bs%2FvHc2H0EJDX2YHf76ZzOuwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1378&quot; height=&quot;392&quot; data-filename=&quot;스크린샷 2026-05-06 오후 10.41.11.png&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;정리하면, 결국 tmux는 터미널 애뮬레이터와 쉘 프로세스 사이에 독립적인 서버를 끼워 넣어서, 에뮬레이터가 꺼져도 쉘 프로세스는 살아있게 만드는 도구인 셈입니다. 그림으로 나타내면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1414&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCvSY3/dJMcaa6fftv/fUhxzBL0QueCCcann98LZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCvSY3/dJMcaa6fftv/fUhxzBL0QueCCcann98LZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCvSY3/dJMcaa6fftv/fUhxzBL0QueCCcann98LZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCvSY3%2FdJMcaa6fftv%2FfUhxzBL0QueCCcann98LZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1414&quot; height=&quot;1102&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;1102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 Ghostty를 닫으면 tmux 클라이언트는 죽지만 tmux 서버와 그 아래 zsh는 살아남아 있게 됩니다. Ghostty를 다시 열고 tmux attach를 입력하면 tmux 서버에 클라이언트가 다시 붙어 해당 쉘 프로세스에서 작업하던 것을 그대로 이어서 할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;패널, 윈도우, 세션&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;tmux에서는 패널, 윈도우, 세션이라는 개념을 사용합니다. 처음에는 세션 == 쉘 프로세스를 의미하는 것으로 오해하기 쉽지만 알고 보면 그렇지 않습니다. tmux 서버 아래로 여러 쉘 프로세스들이 만들어질 수 있는데, tmux가 이들을 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;논리적으로 묶어서 관리하는 단위&lt;/b&gt;&lt;/span&gt;가 바로 패널, 윈도우, 세션입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구조는 세 계층으로 이루어져 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;패널(Pane)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가장 작은 단위를 말하며, 하나하나가 쉘 프로세스에 대응합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;윈도우&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;패널들이 묶인 집단을 일컫는 단위입니다. 하나의 패널은 하나의 윈도우 안에 있고, 윈도우는 하나 이상의 패널로 구성되게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;윈도우들이 묶인 집단을 일컫는 단위입니다. 세션은 하나 이상의 클라이언트에 연결(attach)될 수 있고, 연결되면 클라이언트가 실행 중인 외부 터미널(ex: Ghostty)에도 표시됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;패널 / 윈도우 / 세션의 관계를 도식화해보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1777824277554&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tmux server
  ├── 세션 A (&quot;work&quot;)
  │     ├── 윈도우 1
  │     │     ├── 패널 1 (zsh)
  │     │     └── 패널 2 (zsh)
  │     └── 윈도우 2
  │           └── 패널 1 (zsh)
  └── 세션 B (&quot;personal&quot;)
        └── 윈도우 1
              └── 패널 1 (zsh)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;화면 기준으로 설명하면 다음과 같습니다. 파란색으로 표기한 영역이 패널, 빨간색으로 표기한 영역이 윈도우입니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-06 오후 10.45.48.png&quot; data-origin-width=&quot;3444&quot; data-origin-height=&quot;2128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eFxJEJ/dJMcaarF0lE/XPDVNdp5dpoiT8QkerO9Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eFxJEJ/dJMcaarF0lE/XPDVNdp5dpoiT8QkerO9Tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eFxJEJ/dJMcaarF0lE/XPDVNdp5dpoiT8QkerO9Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeFxJEJ%2FdJMcaarF0lE%2FXPDVNdp5dpoiT8QkerO9Tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3444&quot; height=&quot;2128&quot; data-filename=&quot;스크린샷 2026-05-06 오후 10.45.48.png&quot; data-origin-width=&quot;3444&quot; data-origin-height=&quot;2128&quot;/&gt;&lt;/span&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;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그러면 왜 요즘 들어 tmux가 다시 주목을 받는가&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;최근 AI 에이전트들이 코딩을 해주는 워크플로가 보편화되면서, 그에 맞춰 tmux가 주목을 많이 받고 있는데요. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;사실 tmux는 2000년대에 나온 도구로, 기술의 발전에 따라 최근에 나온 기술이 아닙니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;이 도구가 다시 주목을 받는 이유는 다음처럼 정리해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AI 에이전트는 오래 걸리는 작업을 백그라운드로 돌려둘 일이 많습니다. Claude Code 같은 도구에 개발이나 리팩토링, 또는 테스트 작성을 시켜두면 짧게는 몇 분, 길게는 수십 분이 걸립니다. 복잡한 작업을 시작했는데 회의 때문에 노트북을 닫아야 하는 상황 등이 생겨도 세션을 detach해두면 작업은 계속 돌릴 수 있습니다. 나중에 다시 attach하면 대화 기록과 실행 중이던 프로세스, 진행 중이던 빌드까지 그대로 이어서 할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;멀티 에이전트 기반의 워크플로가 보편화되고 있습니다. 예전에 tmux 화면 분할의 사용 예시는 &quot;코드를 편집하면서 서버 로그도 같이 보고 싶다&quot; 정도로 추측해볼 수 있지만, 이제는 tmux 화면 분할을 통해 멀티 에이전트 기반으로 이뤄지는 작업을 봐야 하는 케이스들이 많습니다. 예를 들면 한 에이전트는 인증 모듈을 리팩토링하고, 다른 에이전트는 API 엔드포인트를 작성하고, 또 다른 에이전트는 테스트를 돌릴 때, 이 모든 걸 한 화면에서 보려면 결국 화면 분할이 필요합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요즘은 클로드 코드 등이 알아서 tmux를 쓰기도 하기 때문에, 좋든 싫든 tmux를 몰라도 한 번쯤은 tmux를 마주하게 되는 것 같습니다. 이 글을 읽는 분들도 이 글을 통해 tmux의 기본 원리에 대해 이해할 수 있었으면 좋겠습니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/그 외 필요한 지식</category>
      <category>AI</category>
      <category>tmux</category>
      <category>터미널</category>
      <category>터미널 분할</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/156</guid>
      <comments>https://jofestudio.tistory.com/156#entry156comment</comments>
      <pubDate>Wed, 6 May 2026 23:32:56 +0900</pubDate>
    </item>
    <item>
      <title>[MyBatis] (1편) MyBatis는 무엇을 해결해주는가? - MyBatis의 구성 요소와 동작 원리</title>
      <link>https://jofestudio.tistory.com/155</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이전 글들에서, JDBC 기반으로 직접 DB를 다루는 방법들을 살펴봤었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a style=&quot;background-color: #f1f1f1; color: #444444; text-align: center;&quot; href=&quot;https://jofestudio.tistory.com/153&quot;&gt;[JDBC] (1편) MyBatis, JPA 없이 JDBC로만 DB를 다루는 세상이었다면&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a style=&quot;background-color: #f1f1f1; color: #444444; text-align: center;&quot; href=&quot;https://jofestudio.tistory.com/154&quot;&gt;[JDBC] (2편) JDBC 기반으로 직접 트랜잭션을 처리해보고, JDBC 스펙에서의 커넥션풀 알아보기&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC만으로 DB에서 데이터들을 가져와서 다루는 예시 코드를 다시 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772351935810&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public List&amp;lt;Customer&amp;gt; getCustomers() {
   List&amp;lt;Customer&amp;gt; customers = new ArrayList&amp;lt;&amp;gt;();
   String selectSql = &quot;SELECT CUST_ID, CUST_NAME, RES_ID, CUST_TYPE FROM TB_CUSTOMER LIMIT ?&quot;;

   try (Connection conn = JdbcSample.getConnectionByDriverManager();
        PreparedStatement pstmt = conn.prepareStatement(selectSql)) {

       pstmt.setInt(1, 10);

       try (ResultSet rs = pstmt.executeQuery()) {
           while (rs.next()) {
               Long custId = rs.getLong(&quot;CUST_ID&quot;);
               String custName = rs.getString(&quot;CUST_NAME&quot;);
               String resId = rs.getString(&quot;RES_ID&quot;);
               String custType = rs.getString(&quot;CUST_TYPE&quot;);

               customers.add(new Customer(custId, custName, resId, custType));
           }
       }

   } catch (SQLException e) {
       System.out.println(&quot;Database error: &quot; + e.getMessage());
   }

   return customers;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;보시다시피 이렇게 JDBC만으로 DB를 접근하게 되면,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SQL 문자열을 작성하는 코드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Connection 얻어오고, PreparedStatement 생성하고, 파라미터를 설정하는 코드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ResultSet을 객체(Customer)로 매핑시키는 코드&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;를 매번 반복해서 작성해야 합니다. Connection, Statement, ResultSet 등을 close하는 것도 계속 신경써줘야 하기도 하구요. 이런 코드에서 중요한 것은 결국 SQL인데, 매번 부수적인 작업들도 직접 반복(&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;보일러플레이트라고도 합니다)&lt;/span&gt;해서 해주는건 힘들 수밖에 없습니다. 또한 SQL문과 자바 코드가 강결합되어 있어 유지보수성이 안 좋아지는 문제도 생기게 됐습니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자연스럽게 SQL은 개발자가 작성하도록 냅두되 나머지 작업들을 자동화하여 보일러플레이트를 제거하자는 니즈가 생기게 됐고, SQL문을 자바 코드로부터 분리하여 보관하자는 니즈도 생기게 됐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 흐름 속에서 나온 기술이 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;SQL Mapper&lt;/b&gt;&lt;/span&gt;입니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번 글에서는 SQL Mapper의 대표 주자인 MyBatis의 동작 원리를 살펴보며, 어떻게 이 문제들을 해결했는지 알아본 것을 공유하고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;목차는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Mybatis란&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Mybatis가 실행할 SQL들을 찾는 방법&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Mybatis가 SQL을 실행하는 방법 (1) - 메서드 호출이 PreparedStatement가 되기까지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Mybatis가 SQL을 실행하는 방법 (2) - MyBatis는 파라미터를 어떻게 자동으로 바인딩하는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Mybatis가 SQL을 실행하는 방법 (3) - ResultSet이 객체가 되기까지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;전체 흐름 정리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Mybatis가 포기한 것&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. Mybatis란&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Mybatis는 앞서 설명했듯 SQL mapper 기술의 일종으로, JDBC로 처리하던 상당 부분을 대신 해주는 프레임워크입니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;핵심 컨셉은 다음과 같이 이해해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;SQL문을 자바 코드에서 분리 보관한다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;SQL문이 문자열로 자바 코드에서 함께 관리가 된다면, 관련 코드가 지저분해지고 DBA와의 협업도 어려워진다는(ex: 개발자와 DBA가 동일한 파일을 수정하게 되는 상황 등) 문제가 생길 수 있습니다. &lt;/span&gt;Mybatis는 SQL문을 자바 코드에서 분리해서 다음과 같이 xml파일에 보관하도록 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772542726187&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;org.example.TestMapper&quot; &amp;gt;
    &amp;lt;select id=&quot;getCustomers&quot; resultType=&quot;org.example.Customer&quot; parameterType=&quot;int&quot;&amp;gt;
        SELECT CUST_ID, CUST_NAME, RES_ID, CUST_TYPE
        FROM TB_CUSTOMER
        LIMIT #{limit}
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;span&gt; 이 글에서는 Mybatis의 사용법이 아닌 원리를 다루려고 하기 때문에,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; resultType 등이 무슨 문법인지 등에 대한 설명은 하지 않으려고 합니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;SQL문들은 개발자가 직접 작성하게 하되, 나머지는 프레임워크가 처리한다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Mybatis를 통해 DB를 접근하는 간단한 예제 코드를 봐보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772542788125&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// (1) SqlSession 요청
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
     // (2) 쿼리 실행 요청
     List&amp;lt;Customer&amp;gt; customers = sqlSession.selectList(&quot;org.example.TestMapper.getCustomers&quot;, 10);

     for (Customer customer : customers) {
          System.out.println(customer);
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존 JDBC만로 DB에 접근하던 것과는 다르게, Connection을 획득하거나 Statement를 생성하는 등의 코드가 없어진 것을 볼 수 있습니다. 다만 중요한 점은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Mybatis는 JDBC를 대체하여 보일러플레이트들을 없애는 기술이 아니라, JDBC 위에서 동작하면서 개발자 대신 Connection 획득이나 파라미터 설정 등의 작업을 대신 해주고 있는 기술&lt;/b&gt;&lt;/span&gt;이라는 점입니다. 위 코드에 나오는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;SqlSession&lt;/b&gt;&lt;/span&gt;이라는 컴포넌트가 그 작업들을 대신 한다고 이해할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉,&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Mybatis를 통해 DB를 접근할 때의 과정은 내부적으로 어떤 일들이 일어나는지는 아직까지는 모르나, 전체 흐름은 간단하게 다음처럼 도식화할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;2796&quot; data-origin-height=&quot;1524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ALGXW/dJMcafFIBKQ/hBcf0ZDNoPBbtFznbhbf6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ALGXW/dJMcafFIBKQ/hBcf0ZDNoPBbtFznbhbf6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ALGXW/dJMcafFIBKQ/hBcf0ZDNoPBbtFznbhbf6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FALGXW%2FdJMcafFIBKQ%2FhBcf0ZDNoPBbtFznbhbf6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2796&quot; height=&quot;1524&quot; data-origin-width=&quot;2796&quot; data-origin-height=&quot;1524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;계속해서 Mybatis가 어떤 구성요소들을 어떻게 활용해 위 그림의 작업들을 개발자 대신 해주고 있는지 볼 텐데, 결국에는 이전에 살펴봤던 JDBC 스펙에서의 인터페이스들이 등장한다는 것을 미리 알아두면 흐름을 따라가는게 좀 더 편할 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※ &lt;/span&gt;다시 한 번 이전에 쓴 글들을 공유합니다. 위에서 나온 PreparedStatement 등의 용어가 낯설다면 1편을 봐보시는 걸 추천드립니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a style=&quot;background-color: #f1f1f1; color: #444444; text-align: center;&quot; href=&quot;https://jofestudio.tistory.com/153&quot;&gt;[JDBC] (1편) MyBatis, JPA 없이 JDBC로만 DB를 다루는 세상이었다면&lt;/a&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a style=&quot;background-color: #f1f1f1; color: #444444; text-align: center;&quot; href=&quot;https://jofestudio.tistory.com/154&quot;&gt;[JDBC] (2편) JDBC 기반으로 직접 트랜잭션을 처리해보고, JDBC 스펙에서의 커넥션풀 알아보기&lt;/a&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. Mybatis가 실행할 SQL들을 찾는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;애플리케이션이 특정 SQL의 실행을 요청하면, Mybatis는 해당 SQL을 찾아서 실행해주게 됩니다. Mybatis의 핵심 컨셉 중 하나는 SQL문을 코드에서 분리해서 보관하는 것이었고, 그로 인해 xml파일에 따로 SQL들을 다음과 같이 가지고 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772544186372&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;org.example.TestMapper&quot; &amp;gt;
    &amp;lt;select id=&quot;getCustomers&quot; resultType=&quot;org.example.Customer&quot; parameterType=&quot;int&quot;&amp;gt;
        SELECT CUST_ID, CUST_NAME, RES_ID, CUST_TYPE
        FROM TB_CUSTOMER
        LIMIT #{limit}
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 애플리케이션이 SQL 실행을 요청할 때마다 xml파일을 디스크에서 읽는다면, 매 요청마다 파일 I/O는 물론 xml을 파싱하는 비용 등이 들게 됩니다. 그래서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Mybatis는 애플리케이션이 시작될 때 xml파일을 딱 한 번 파싱하고, 그 결과를 메모리에 두는 방식을 사용&lt;/b&gt;&lt;/span&gt;하기로 했습니다. 다만 파싱한 결과로써 SQL문 하나만 가지고 있는 건 의미가 없습니다. 따라서&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 SQL의 식별자(id값)이 뭔지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 SQL이 어떤 타입의 SQL인지(SELECT, INSERT 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 SQL에 어떤 파라미터들이 쓰이는지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 SQL의 결과를 어떤 DTO로 매핑해야 하는지&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;등 파싱하여 얻을 수 있는 다른 정보들도 가지고 있게 했고, Mybatis은 이 정보들을 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;MappedStatement&lt;/b&gt;&lt;/span&gt;라는 객체에 저장하게 했습니다. xml과 MappedStatement의 관계를 간단하게 도식화하면 다음과 같습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;2280&quot; data-origin-height=&quot;1404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZhvJu/dJMcaaYHSTw/6LLNjqTKh24KPyhAzt8Rn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZhvJu/dJMcaaYHSTw/6LLNjqTKh24KPyhAzt8Rn1/img.png&quot; data-alt=&quot;보시다시피 xml에서 작성한 &amp;amp;lt;insert&amp;amp;gt;, &amp;amp;lt;select&amp;amp;gt;, &amp;amp;lt;update&amp;amp;gt;, &amp;amp;lt;delete&amp;amp;gt; 태그들 하나당 MappedStatemet라는 객체가 만들어져 메모리에 캐싱되게 됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZhvJu/dJMcaaYHSTw/6LLNjqTKh24KPyhAzt8Rn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZhvJu%2FdJMcaaYHSTw%2F6LLNjqTKh24KPyhAzt8Rn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2280&quot; height=&quot;1404&quot; data-origin-width=&quot;2280&quot; data-origin-height=&quot;1404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;보시다시피 xml에서 작성한 &amp;lt;insert&amp;gt;, &amp;lt;select&amp;gt;, &amp;lt;update&amp;gt;, &amp;lt;delete&amp;gt; 태그들 하나당 MappedStatemet라는 객체가 만들어져 메모리에 캐싱되게 됩니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그러면 자연스럽게 &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;xml파일들을 파싱해서 MappedStatement를 생성하는 작업은 언제 어떻게 누가 하는지에 대한 의문이 생깁니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Mybatis를 사용 시 Mybatis 설정 정보들을 mybatis-config.xml에 작성하고, SQL문들이 있는 xml들의 위치를 적어주는데요. Mybatis를 사용할 땐 이 정보들을 바탕으로 애플리케이션을 기동할 때 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;SqlSessionFactory라는 컴포넌트를 생성해야 하는데, &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;바로 이 과정에서 MappedStatement들이 생성됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;mybatis-config.xml이 아닌 다른 이름을 써도 됩니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;SQL문들이 있는 xml들을 mapper xml이라고도 부릅니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;MappedStatement는 Map&amp;lt;String,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;MappedStatement&amp;gt; 형태로 메모리에 캐싱되며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;id값이 해당 SQL을 찾는 키값이 됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;SqlSessionFactory를 생성하는 예제 코드는 다음과 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772639407616&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// mybatis-config.xml을 classpath에서 읽어서 사용
InputStream inputStream = Resources.getResourceAsStream(&quot;mybatis-config.xml&quot;);

// SqlSessionFactoryBuilder를 통해 sqlSessionFactory 생성 (이 과정에서 xml들을 파싱)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;디버깅을 통해 실제 코드 흐름을 따라가보면 다음과 같이 mybatis-config.xml을 파싱한 뒤, 파싱 결과를 바탕으로 mapper xml들을 파싱하며 MappedStatement들을 생성하는 과정을 볼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772639848138&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// XMLConfigBuilder.class  -&amp;gt;  mybatis-config.xml을 파싱하는 역할
// 제가 임의로 간결하게 작성했습니다.

public Configuration parse() {
   this.parseConfiguration(this.parser.evalNode(&quot;/configuration&quot;));
   return this.configuration;
}


// XMLMapperBuilder.class  -&amp;gt;  mapper xml을 파싱하는 역할
// 제가 임의로 간결하게 작성했습니다.

private void configurationElement(XNode context) {
    this.parameterMapElement(context.evalNodes(&quot;/mapper/parameterMap&quot;));
    this.resultMapElements(context.evalNodes(&quot;/mapper/resultMap&quot;));
    this.sqlElement(context.evalNodes(&quot;/mapper/sql&quot;));
    this.buildStatementFromContext(context.evalNodes(&quot;select|insert|update|delete&quot;));
}


// MapperBuilderAssistant.class  -&amp;gt;  MappedStatement를 등록하는 역할
// 제가 임의로 간결하게 작성했습니다.

public MappedStatement addMappedStatement(String id, SqlSource sqlSource, ... 생략) {
    MappedStatement statement = statementBuilder.build();
    this.configuration.addMappedStatement(statement);
    return statement;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;즉 SqlSessionFactory가 생성되면서 MappedStatement를 만드는 과정을 간단히 도식화하면 다음과 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2618&quot; data-origin-height=&quot;1778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Khd79/dJMb99Mgefi/KtrZAse4fqYLxe9KCoO1BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Khd79/dJMb99Mgefi/KtrZAse4fqYLxe9KCoO1BK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Khd79/dJMb99Mgefi/KtrZAse4fqYLxe9KCoO1BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKhd79%2FdJMb99Mgefi%2FKtrZAse4fqYLxe9KCoO1BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2618&quot; height=&quot;1778&quot; data-origin-width=&quot;2618&quot; data-origin-height=&quot;1778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 SqlSessionFactory가 생성될 때 갖고 있게 되는 Configuration 객체를 보면, 다음과 같이 MappedStatement와 관련 메서드를 가지고 있는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773062301971&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Configuration.class

public class Configuration {

    // ... 생략

    protected final Map&amp;lt;String, MappedStatement&amp;gt; mappedStatements;

    public void addMappedStatement(MappedStatement ms) {
        // ... 생략
    }

    public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
        // ... 생략
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&amp;nbsp;&lt;/span&gt;Configuration: MyBatis가 XML로부터 파싱한 모든 설정정보 등을 담고 있는 객체&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. Mybatis가 SQL을 실행하는 방법 (1) - 메서드 호출이 PreparedStatement가 되기까지&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; letter-spacing: 0px;&quot;&gt;Mybatis를 통해 SQL을 실행하게 되면, 처음에 말씀드렸듯이 Connection을 획득하고 PrepatedStatement를 만든 뒤, 파라미터들을 설정하여 쿼리를 실행하고 ResultSet을 DTO로 변환하는 과정을 내부적으로 수행하게 됩니다. 앞서 살펴본 MappedStatement를 바탕으로 이 과정을 어떻게 하는건지 하나씩 살펴보려고 하며, 우선 Connection 획득과 PreparedStatement 생성을 어떻게 하는지부터 보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 위해, Mybatis가 사용하는 다음 구성 요소들을 알고 넘어가야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SqlSession&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;애플리케이션과의 인터페이스 역할을 하는 요소입니다&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;애플리케이션으로부터 특정 SQL의 실행 요청을 받아, 내부적으론 Executor에게 실행을 위임하여 처리합니다&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Executor&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Mybatis의 실행 엔진 역할을 하는 요소입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SqlSession에게 위임받은 작업을 처리하기 위해 StatementHandler를 생성하고 쿼리 실행 흐름을 조율합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SqlSessionFactory가 SqlSession을 만들어줄 때마다 SqlSession당 하나씩 Executor의 구현체를 만들어줍니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 Configuration이 들고 있는 ExecutorType(SIMPLE, REUSE, BATCH)에 따라 서로 다른 구현체를 갖게 됩니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;StatementHandler&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC Statement 계열의 객체를 준비하고, 파라미터를 설정과 실제 쿼리/업데이트를 실행하는 역할의 인터페이스입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Executor가 쿼리 실행시점에 내부적으로 생성하여 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(1) MappedStatement에서 실제로 실행할 SQL 가져오기 (BoundSql)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;BoundSql&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MappedStatement가 가지고 있는 SQL문 정보로부터 동적콘텐츠들을 처리한 후 가져온 실제로 실행 예정인 &quot;SQL 문자열&quot;을 의미하는 객체입니다. (&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;MappedStatement가 기본적으로 가지고 있는 SQL문은 동적콘텐츠(&amp;lt;if&amp;gt; 등..)을 가지고 있습니다)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;파라미터 매핑 정보(ParameterMappings)도 함께 가지고 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MappedStatement가 SQL문의 정보를 가지고 있는 건 맞습니다만, xml파일에는 SQL문을 다음처럼 동적콘텐츠들을 활용해 작성할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773065896189&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;getCustomersByCustomerType&quot; resultType=&quot;org.example.Customer&quot; parameterType=&quot;map&quot;&amp;gt;
    SELECT CUST_ID, CUST_NAME, RES_ID, CUST_TYPE
    FROM TB_CUSTOMER
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;customerType != null&quot;&amp;gt;
            AND CUST_TYPE = #{customerType}
        &amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
    LIMIT #{limit}
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 경우 실행시점에 실질적으로 어떤 SQL이 수행되는건지 결정되어야 합니다만, MappedStatement를 만드는 시점 즉 애플리케이션 구동 시점에 이를 결정하는 건 불가능합니다. 따라서 MappedStatement는 런타임에 파라미터 정보를 바탕으로 실질적으로 실행되어야 하는 SQL문을 BoundSql이란 객체로 만들어서 리턴하는 메서드를 제공해주고 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 애플리케이션이 쿼리 실행 요청을 보내면, MappedStatement를 활용해 다음처럼 BoundSql을 가져와서 쿼리의 실행에 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773066482432&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// (1) 애플리케이션에서 쿼리 실행 요청
List&amp;lt;Customer&amp;gt; customers = sqlSession.selectList(&quot;org.example.TestMapper.getCustomers&quot;, 10);


// (2) DefaultSqlSession.class - MappedStatement를 찾아서 실행 
// 제가 임의로 간결하게 작성했습니다
private &amp;lt;E&amp;gt; List&amp;lt;E&amp;gt; selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    MappedStatement ms = this.configuration.getMappedStatement(statement);
    this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
}


// (3) CachingExecutor.class - MappedStatement로부터 BoundSql 획득하여 실행
public &amp;lt;E&amp;gt; List&amp;lt;E&amp;gt; query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 CachingExecutor는 SimpleDecorator를 멤버변수로 갖고 있는 Executor로, 2차 캐싱 적용을 위한 일종의 데코레이터 패턴을 적용한 요소로 이해할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2) Connection 획득 ~ PreparedStatement 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이후의 흐름을 살펴보면, 가져온 BoundSql을 활용하여 StatementHandler를 생성하여 쿼리를 실행하게 됩니다. 이 부분을 뜯어보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773065205863&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SimpleExecutor.class
// 제가 임의로 간결하게 작성했습니다.

public &amp;lt;E&amp;gt; List&amp;lt;E&amp;gt; doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    
    // (1) StatementHandler 생성
    StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

    // (2) Connection 획득 + PreparedStatement 생성 + 파라미터 설정
    Statement stmt = this.prepareStatement(handler, ms.getStatementLog());

    // (3) 쿼리 실행
    List var9 = handler.query(stmt, resultHandler);

    return var9;
}

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    // Connection 획득
    Connection connection = this.getConnection(statementLog);
    
    // PreparedStatement 생성
    Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
    
    // 파라미터 설정 (이 부분은 다음 섹션에서 설명합니다)
    handler.parameterize(stmt);
    
    return stmt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Connection&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;을 획득하는 코드를 따라가보면, 다음처럼 JDBC에서 쓰던 DataSource.getConnection을 통해 Connection을 획득하는 것을 볼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773067245378&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// JDBCTransaction.class

protected void openConnection() throws SQLException {
    // ... 생략

    this.connection = this.dataSource.getConnection();
    
    // ... 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;또한 PreparedStatment를 생성하는 코드를 따라가보면, BoundSql과 Connection을 활용해 PreparedStatement를 생성하는 것을 볼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773067338929&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PreparedStatementHandler.class

protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = this.boundSql.getSql();
    if (this.mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        String[] keyColumnNames = this.mappedStatement.getKeyColumns();
        return keyColumnNames == null ? connection.prepareStatement(sql, 1) : connection.prepareStatement(sql, keyColumnNames);
    } else {
        return this.mappedStatement.getResultSetType() == ResultSetType.DEFAULT ? connection.prepareStatement(sql) : connection.prepareStatement(sql, this.mappedStatement.getResultSetType().getValue(), 1007);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;파라미터를 설정하는 부분은 설명할 내용들이 조금 있어, 바로 아래에 별도 챕터로 기재합니다&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. Mybatis가 SQL을 실행하는 방법 (2) - MyBatis는 파라미터를 어떻게 자동으로 바인딩하는가&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ParameterHandler&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;StatementHandler가 멤버변수로 들고 있는 필드입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PreparedStatement의 파라미터를 설정하는 역할의 인터페이스입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Executor가 StatementHandler의 parameterize 메서드를 호출하면 StatementHandler는 내부적으로 ParameterHandler에게 파라미터 바인딩를 위임합니다. ParameterHandler가 어떻게 바인딩하는지 살펴보려면 앞서 xml파일들을 스캔하며 MappedStatement들을 만들 때 해당 SQL의 파라미터 정보들을 어떻게 들고 있는지 봐야 합니다.&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;2992&quot; data-origin-height=&quot;1582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjVD92/dJMcaiJiNis/RILEc0TSKvCTYCgsBFQYV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjVD92/dJMcaiJiNis/RILEc0TSKvCTYCgsBFQYV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjVD92/dJMcaiJiNis/RILEc0TSKvCTYCgsBFQYV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjVD92%2FdJMcaiJiNis%2FRILEc0TSKvCTYCgsBFQYV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;1582&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;1582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;보시는 것처럼 SQL의 파라미터 부분(#{...}을 말합니다)들을 객체로 만들어서 MappedStatement에 담게 되는데요, 이 객체들을 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;ParameterMapping&lt;/b&gt;&lt;/span&gt;이라고 부릅니다. 이 객체는 여러 속성이 있겠으나 주요 속성들을 설명드리면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;property : 해당 파라미터의 이름을 의미합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;javaType : 해당 파라미터의 java 클래스 타입을 의미합니다. 기본적으론 Object이나, parameterType으로 준 클래스에 해당 필드의 getter가 있다면 해당 getter의 리턴 타입으로 설정됩니다. 또한 SQL에서 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;파라미터를 작성할 때 #{customerType, javaType=string} 처럼 명시적으로 적어줄 수도 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;jdbcType : JDBC는 자바 기반 프로그램이 어떤 DB와 붙어도 컬럼 타입들을 공통으로 가리킬 수 있는 표준 상수들을 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;java.sql.type에 정의하고 있는데요. Mybatis는 편한 사용을 위해 이 상수들을 감싸서 객체화했는데 이를 JDBC Type이라고 부릅니다. 즉 DB에서의 컬럼타입을 의미한다고 이해할 수 있습니다. ParameterMapping에서 이 속성은&amp;nbsp;&lt;/span&gt;기본적으론 null이나, SQL에서 파라미터를 작성할 때 #{customerType, jdbcType=VARCHAR} 처럼 적어주면 해당 값으로 세팅됩니다. 참고로 ParameterMapping에서의 jdbcType은 파라미터로 null값을 세팅하게 되는 경우 등에 활용됩니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;typeHandler : &lt;span style=&quot;color: #282a2c; text-align: start;&quot;&gt;PreparedStatement에 파라미터 세팅을 하려면 파라미터 타입에 따라 setInt, setString 등 어떤 메서드들을 호출해야 하는지 결정해줘야 하는데요, 그 역할을 TypeHandler가 해줍니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;즉&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #282a2c; text-align: start;&quot;&gt;PreparedStatement에 파라미터를 바인딩할 때, java 객체를 적절한 JDBC Type으로 변환해주는 역할로 이해할 수 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;PameterMapping 생성 시 javaType속성값과 jdbcType속성값에 따라 Mybatis가 만들어둔 typeHandler들 중 하나가 선택되며, 개발자가 직접 커스텀한 typeHandler를 사용할 수도 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 사용자가 쿼리 실행을 요청하게 되면, ParameterHandler가 각 ParameterMapping의 정보들을 활용해 사용자가 넘겨준 객체로부터 적절한 값들을 뽑아내서 PreparedStatement에 파라미터를 바인딩하게 됩니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start;&quot;&gt;사용자가 넘겨준 객체(자바빈 객체 또는 Map 등)에서 적절한 값을 빼오는 것은 리플렉션 등으로 구현되어 있으며, 파라미터를 바인딩하는 흐름을 코드로 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773491173995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// DefaultParameterHandler.class
// 제가 임의로 메서드를 간결화하여 작성했습니다

public void setParameters(PreparedStatement ps) {
    List&amp;lt;ParameterMapping&amp;gt; parameterMappings = this.boundSql.getParameterMappings();
    MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);

    for (int i = 0; i &amp;lt; parameterMappings.size(); ++i) {
        ParameterMapping parameterMapping = (ParameterMapping) parameterMappings.get(i);

        String propertyName = parameterMapping.getProperty();
        // 파라미터로 넣을 값. 리플렉션 등을 활용해 가져옴
        Object value = metaObject.getValue(propertyName);

        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();

        typeHandler.setParameter(ps, i + 1, value, jdbcType);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start;&quot;&gt;위 코드의 마지막 부분을 좀 더 따라가면, TypeHandler의 종류에 따라 최종적으로 PreparedStatement에 다음처럼 파라미터를 세팅하는 모습을 볼 수 있습니다. 예시 코드는 TypeHandler가 StringTypeHandler인 경우입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773491272093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// StringTypeHandler.class

public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
    ps.setString(i, parameter);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;5. Mybatis가 SQL을 실행하는 방법 (3) - ResultSet이 객체가 되기까지&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ResultSetHandler&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;StatementHandler가 멤버변수로 들고 있는 필드입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ResultSet를 읽어서 resultType, resultMap 기준으로 자바 객체로 변환하는 역할의 인터페이스입니다.&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ResultSet을 DTO로 매핑하는 것은 ResultSetHandler라는 컴포넌트가 담당하며, 어떻게 매핑하는지 살펴보려면 앞서 xml파일들을 스캔하며 MappedStatement들을 만들 때 해당 SQL의 ResultSet 매핑 정보를 어떻게 들고 있는지 봐야 합니다.&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;2166&quot; data-origin-height=&quot;1726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l9hQg/dJMcaiWP1n1/wkkSj2IhwPcorowzvUOYik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l9hQg/dJMcaiWP1n1/wkkSj2IhwPcorowzvUOYik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l9hQg/dJMcaiWP1n1/wkkSj2IhwPcorowzvUOYik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl9hQg%2FdJMcaiWP1n1%2FwkkSj2IhwPcorowzvUOYik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2166&quot; height=&quot;1726&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;1726&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;보시는 것처럼 resultMap 태그에서 작성해준 정보들을 객체로 만들어서 MappedStatement에 담게 되는데요, 이 객체들을 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;ResultMapping&lt;/b&gt;&lt;/span&gt;이라고 부릅니다. 말 그대로 DB에서 가져온 데이터를 객체에 어떻게 매핑해줄지(어떤 컬럼을 어떤 필드에 매핑할지)를 정의하는 객체입니다. 여러 속성들이 있으나 주요 속성들을 설명드리면 다음과 같습니다. (ParameterMapping에서의 속성들과 매우 유사합니다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;property : 해당 컬럼값이 매핑될 객체의 필드명을 의미합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;column : DB에서의 컬럼명을 의미합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;javaType : 해당 컬럼값이 어떤 java 클래스 타입으로 매핑될지를 의미합니다. DTO에서 해당 필드의 setter가 받는 파라미터 타입으로 설정되며 기본적으론 Object입니다. result 태그 등에서 &amp;lt;result property=&quot;customerName&quot;, javaType=&quot;String&quot; ... /&amp;gt; 처럼 명시적으로 적어줄 수도 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;jdbcType : ParameterMapping에서와 마찬가지로&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt; DB에서의 컬럼타입을 의미한다고 이해할 수 있습니다. &lt;/span&gt;기본적으론 null이나, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;&amp;lt;result property=&quot;customerName&quot;, jdbcType=&quot;VARCHAR&quot; ... /&amp;gt;&lt;span&gt; 처럼 명시적으로 적어줄 수도 있습니다. 참고로 ResultMapping에서의 jdbcType은 typeHandler 결정 시에 사용됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;typeHandler :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #282a2c; text-align: start;&quot;&gt;ResultSet을 DTO에 매핑하려면 DB에서 가져온 컬럼의 타입에 따라 getInt, getString 등 어떤 메서드들을 호출해야 하는지 결정해줘야 하는데요, 그 역할을 TypeHandler가 해줍니다&lt;span style=&quot;color: #282a2c; text-align: start;&quot;&gt;.&lt;span&gt; ParameterMapping에서의 TypeHandler의 역할과 반대인 것으로 이해할 수 있습니다. &lt;/span&gt;&lt;/span&gt;ResultMapping 생성 시 javaType값과 jdbcType값에 따라 Mybatis가 만들어둔 typeHandler들 중 하나가 선택됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 사용자가 요청한 쿼리가 실행되고 나면, ResultSetHandler가 각 ResultMapping의 정보들을 활용해 ResultSet으로부터&amp;nbsp; 적절한 값들을 뽑아내서 DTO 객체의 필드들에 매핑하게 됩니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773666952341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// DefaultResultSetHandler.class
// 제가 임의로 메서드를 간결화하여 작성했습니다

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    // DTO 객체 생성
    Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    
    MetaObject metaObject = this.configuration.newMetaObject(rowValue);
    // ResultMappings 순회하면서 DTO에 setter로 값 세팅
    this.applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix);

    return rowValue;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;applyPropertyMappings 메서드 흐름을 좀 더 따라가보면, ResultMapping의 TypeHandler를 통해 ResultSet으로부터 적절한 getter를 사용하여 컬럼값을 빼내고, DTO의 setter를 호출하여 값을 세팅하는 모습을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773667579129&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// DefaultResultSetHandler.class
// 제가 임의로 메서드를 간결화하여 작성했습니다

private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    boolean foundValues = false;

    for (ResultMapping propertyMapping : resultMap.getPropertyResultMappings()) {
        // ResultMapping 정보 활용해 ResultSet으로부터 컬럼값 추출
        Object value = this.getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
        String property = propertyMapping.getProperty();

        if (value != null) foundValues = true;
        // DTO 객체의 setter를 호출하여 값 세팅
        if (property != null &amp;amp;&amp;amp; value != null) {
            metaObject.setValue(property, value);
        }
    }

    return foundValues;
}

private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    TypeHandler&amp;lt;?&amp;gt; typeHandler = propertyMapping.getTypeHandler();
    String column = this.prependPrefix(propertyMapping.getColumn(), columnPrefix);
    // ResultMapping의 타입핸들러를 꺼내서 ResultSet으로부터 컬럼값 추출
    return typeHandler.getResult(rs, column);
}


// StringTypeHandler.class

public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return rs.getString(columnName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6. 전체 흐름 정리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Mybatis를 통해 DB를 접근할 때의 과정은 간단하게 다음처럼 도식화할 수 있다고 말씀드렸습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;2796&quot; data-origin-height=&quot;1524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ALGXW/dJMcafFIBKQ/hBcf0ZDNoPBbtFznbhbf6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ALGXW/dJMcafFIBKQ/hBcf0ZDNoPBbtFznbhbf6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ALGXW/dJMcafFIBKQ/hBcf0ZDNoPBbtFznbhbf6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FALGXW%2FdJMcafFIBKQ%2FhBcf0ZDNoPBbtFznbhbf6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2796&quot; height=&quot;1524&quot; data-origin-width=&quot;2796&quot; data-origin-height=&quot;1524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 Mybatis의 동작 원리를 살펴봤으니, 위 그림을 좀 더 세분화해서 다음처럼 나타낼 수 있습니다.&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;3008&quot; data-origin-height=&quot;1672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b59NkG/dJMcaaEAf3b/KUo8ERzXCNPCIkobig5s91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b59NkG/dJMcaaEAf3b/KUo8ERzXCNPCIkobig5s91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b59NkG/dJMcaaEAf3b/KUo8ERzXCNPCIkobig5s91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb59NkG%2FdJMcaaEAf3b%2FKUo8ERzXCNPCIkobig5s91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3008&quot; height=&quot;1672&quot; data-origin-width=&quot;3008&quot; data-origin-height=&quot;1672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;7. Mybatis가 포기한 것&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지금까지 Mybatis가 기존에 JDBC로 DB에 접근할 때 발생하던 보일러 플레이트들을 어떻게 해결하는지 살펴봤습니다. 하지만 모든 기술이 그러하듯 장점만 있을 수는 없는데요, Mybatis도 포기한 문제(내지는 해결하지 못한 문제)가 존재합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 설명했듯 Mybatis의 핵심 컨셉은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;SQL문들은 개발자가 직접 작성하게 하되, 나머지는 프레임워크가 처리한다&quot;&lt;/b&gt;&lt;/span&gt;입니다. 이 말은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;개발자가 관계형 DB의 방식으로 사고해야 한다&lt;/b&gt;&lt;/span&gt;는 한 가지 전제를 포함하는 의미이기도 합니다. 데이터를 테이블로 보고, 관계를 JOIN으로 표현하고, 결과를 행과 컬럼으로 다뤄야 한다는 뜻입니다. 이는 Java라는 언어가 갖는 객체 지향적 관점과는 다른 패러다임을 갖는 것이기 때문에, 개발자가 두 언어의 차이점을 중간에서 메꿔주는 데 시간을 많이 쏟게 되는 문제가 남게 됩니다. 예를 들면 코드베이스가 커질수록 단순한 CRUD도 모든 엔티티마다 추가해줘야 하고, 연관관계가 복잡해질수록 resultMap도 복잡해지고, DB 스키마가 바뀌면 SQL도 직접 바꿔줘야 하는 것 등이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 &quot;SQL도 개발자가 직접 쓰지 않게 하는 건 어떨까?&quot; 라는 생각이 나오게 됐고, 후에 객체와 객체 간의 관계만 정의하면, SQL 생성부터 연관 객체 로딩까지 프레임워크가 처리하는 ORM 기술들이 나오게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리하면 MyBatis는 &quot;JDBC를 통한 DB 직접 접근&quot;의 보일러 플레이트를 제거하는 데 성공했지만, 여전히 SQL 중심으로 사고를 요구하게 됩니다. 복잡한 집계나 추출 등 SQL을 직접 제어해야 하는 요구사항이 있는 시스템들이 존재하므로, &quot;SQL 중심의 사고를 요구&quot;한다는 것은 단점이 아닌 패러다임으로 봐야 합니다. 다만 이는 객체 중심 사고와의 간극이 존재하는 것이므로, SQL 중심의 사고 및 제어가 필요하면 Mybatis같은 SQL Mapper를, 객체 중심의 사고가 필요하다면 ORM을 선택하는 것이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;레퍼런스&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://mybatis.org/mybatis-3/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mybatis.org/mybatis-3/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://mybatis.org/mybatis-3/apidocs/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mybatis.org/mybatis-3/apidocs/index.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/JAVA</category>
      <category>JDBC</category>
      <category>JPA</category>
      <category>MappedStatement</category>
      <category>MyBatis</category>
      <category>mybatis 구성요소</category>
      <category>Mybatis 동작원리</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/155</guid>
      <comments>https://jofestudio.tistory.com/155#entry155comment</comments>
      <pubDate>Tue, 17 Mar 2026 23:21:37 +0900</pubDate>
    </item>
    <item>
      <title>[JDBC] (2편) JDBC 기반으로 직접 트랜잭션을 처리해보고, JDBC 스펙에서의 커넥션풀 알아보기</title>
      <link>https://jofestudio.tistory.com/154</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지난 글에서는 MyBatis나 JPA없이 순수 JDBC만을 활용해 &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Connection, Statement, ResultSet 등을 사용하면서 &lt;/span&gt;DB에 접근하는 방법을 다뤘었습니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;/span&gt;지난 글 : &lt;a href=&quot;https://jofestudio.tistory.com/153&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jofestudio.tistory.com/153&lt;/a&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;애플리케이션을 개발하면서 @Transactional 어노테이션을 통해 트랜잭션을 편리하게 사용하기도 하고, 커넥션풀들도 이미 사용 중이기도 한데요. JDBC만으로는 트랜잭션을 어떻게 다루고, 또 JDBC 스펙에서는 커넥션풀을 어떻게 다루도록 설계했는지 그 원리가 궁금하여 이번에 공부를 했고, 그 내용을 공유하고자 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;목차는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;J&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DBC로 트랜잭션 직접 다루기&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Auto Commit 모드 끄기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;rollback 메서드를 명시적으로 쓰지 않으면 어떻게 될까?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SavePoint 인터페이스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC와 트랜잭션 격리 수준&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커넥션 풀&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ConnectionPoolDataSource&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PooledConnection&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;⭐️ 하지만 실제로 이 설계들을 항상 지키진 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커넥션 풀 관련 속성&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. JDBC로 트랜잭션 직접 다루기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1 - 1. Auto Commit 모드 끄기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC는 Connection 객체가 만들어지면, Auto Commit 모드가 켜진 채로 만들어지는 것을 디폴트로 하고 있습니다. 이는 각 SQL문들이 서로 독립적인 트랜잭션으로 실행되며 실행된 후에 곧바로 커밋된다는 것을 의미하는데요, 여러 SQL문들을 하나의 트랜잭션으로 처리하고 싶으면 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Auto Commit&lt;/span&gt; 모드를 다음과 같이 직접 꺼줘야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771659666084&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;conn.setAutoCommit(false);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하고 나면, 직접 명시적으로 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Connection&lt;/span&gt; 객체의 commit 메서드를 호출해야만 트랜잭션이 커밋됩니다. 예외가 발생한 경우, rollback 메서드를 명시적으로 호출할 것을 권고하고 있습니다. 예시는 다음과 같습니다. (제가 토이 프로젝트로 하고 있는 프로젝트의 테이블을 사용해습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771661770565&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void insertNewProduct() {
   String insertProductSql = &quot;INSERT INTO TB_PRODUCT (PROD_CODE, PROD_NAME, PROD_TYPE) values (?, ?, ?)&quot;;
   String insertProductHistorySql = &quot;INSERT INTO TB_PRODUCT_HISTORY &quot; +
           &quot;(PROD_CODE, PROD_NAME, PROD_TYPE, PROCESSED_TYPE) VALUES (?, ?, ?, ?)&quot;;

   try (Connection conn = JdbcSample.getConnectionByDriverManager()) {
       conn.setAutoCommit(false); // 오토 커밋 꺼주기

       try (PreparedStatement insertProductPstmt = conn.prepareStatement(insertProductSql);
            PreparedStatement insertProductHistoryPstmt = conn.prepareStatement(insertProductHistorySql)) {

           insertProductPstmt.setString(1, &quot;600&quot;);
           insertProductPstmt.setString(2, &quot;테스트입출금통장&quot;);
           insertProductPstmt.setString(3, &quot;10&quot;);
           insertProductPstmt.executeUpdate();

           insertProductHistoryPstmt.setString(1, &quot;600&quot;);
           insertProductHistoryPstmt.setString(2, &quot;테스트입출금통장&quot;);
           insertProductHistoryPstmt.setString(3, &quot;10&quot;);
           insertProductHistoryPstmt.setString(4, &quot;I&quot;);
           insertProductHistoryPstmt.executeUpdate();

           conn.commit(); // 커밋 메서드 명시

       } catch (SQLException e) {
           conn.rollback();
           throw e;
       }

   } catch (SQLException e) {
       System.out.println(&quot;Database error: &quot; + e.getMessage());
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고) &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MySQL JDBC Driver 기준, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Connection 생성 시 SET autocommit=1이 DB로 날아가고, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Auto Commit 모드를&lt;/span&gt; 끄면 SET autocommit=0이 DB로 날아갑니다 .&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1 - 2. rollback 메서드를 명시적으로 쓰지 않으면 어떻게 될까?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예외가 발생하면 rollback 메서드를 호출해야 한다고 하지만, 그럼 만약 예외 발생 시 rollback 메서드를 명시적으로 사용하지 않으면 롤백이 안 되는 걸까요? 뜯어보니 MySQL JDBC Driver가 제공하는 Connection 구현체 중 ConnectionImpl은 close될 때 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Auto Commit&lt;/span&gt; 모드를 꺼둔 상태라면 롤백을 해주도록 되어있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;736&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNxcBC/dJMcaih1Mau/aejPPorHFMeDDIqkZVfaU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNxcBC/dJMcaih1Mau/aejPPorHFMeDDIqkZVfaU1/img.png&quot; data-alt=&quot;MySQL JDBC Driver 內 ConnectionImpl의 doClose 메서드 내부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNxcBC/dJMcaih1Mau/aejPPorHFMeDDIqkZVfaU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNxcBC%2FdJMcaih1Mau%2FaejPPorHFMeDDIqkZVfaU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;238&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySQL JDBC Driver 內 ConnectionImpl의 doClose 메서드 내부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Auto Commit 모드&lt;/span&gt;를 끈 상태라면 좌우지간 개발자가 명시적으로 commit 메서드를 호출하지 않아도 롤백이 자동으로 되고, 예외가 터졌을 때도 명시적으로 rollback 메서드를 호출하지 않아도 롤백이 자동으로 되긴 합니다. 심지어 commit 메서드를 수행한 뒤에도 close될 때 ROLLBACK이 DB로 날라가는 모습도 확인했었습니다. 그러면 굳이 rollback 메서드를 호출하지 않아도 되는 것 아니냐는 생각을 했습니다만, DataSource 구현체를 어떤 것을 사용하는지에 따라 사용하는 Connection 구현체가 바뀌는 등 드라이버의 처리에 의존하는 것보다는 개발자가 명시적으로 rollback 메서드를 호출하는 것이 안정성 있는 시스템을 만들 수 있다는 생각이 듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1 - 3. SavePoint 인터페이스&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC API는 SavePoint라는 인터페이스를 통해 rollback 메서드 수행 시 특정 지점으로 되돌아가는 기능도 제공합니다. 트랜잭션 전체를 롤백하지 않고, Savepoint가 설정된 곳 이후의 작업만 롤백하는 형태로 이해할 수 있습니다. Connection 인터페이스의 다음 메서드들을 통해 SavePoint들을 다룰 수 있습니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;setSavePoint() : 현재 트랜잭션에 익명의 SavePoint를 만들어 리턴&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;setSavePoint(String) : 현재 트랜잭션에 이름이 있는 SavePoint를 만들어 리턴&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;rollback(SavePoint) : SavePoint가 set된 곳 이후의 작업들을 undo (Auto commit이 꺼져있을 때 사용되어야 합니다)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;releaseSavePoint(SavePoint) : 현재 트랜잭션에서 해당 SavePoint를 포함해 이후에 설정된 SavePoint들을 전부 해제 (한 트랜잭션이 과도하게 많은 SavePoint를 set하면 그만큼 DB 리소스를 많이 쓰는 거라, 이 메서드를 통해 SavePoint들을 중간중간 제거하여 리소스를 해제하는 역할을 할 수 있습니다)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;예제는 다음과 같습니다. 아까 보여드린 코드를 재활용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771848625753&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void insertNewProduct() {
   String insertProductSql = &quot;INSERT INTO TB_PRODUCT (PROD_CODE, PROD_NAME, PROD_TYPE) values (?, ?, ?)&quot;;
   String insertProductHistorySql = &quot;INSERT INTO TB_PRODUCT_HISTORY &quot; +
           &quot;(PROD_CODE, PROD_NAME, PROD_TYPE, PROCESSED_TYPE) VALUES (?, ?, ?, ?)&quot;;

   try (Connection conn = JdbcSample.getConnectionByDataSource()) {
       conn.setAutoCommit(false);

       Savepoint savepoint = conn.setSavepoint(&quot;BeforeInsert&quot;);

       try (PreparedStatement insertProductPstmt = conn.prepareStatement(insertProductSql);
            PreparedStatement insertProductHistoryPstmt = conn.prepareStatement(insertProductHistorySql)) {

           insertProductPstmt.setString(1, &quot;600&quot;);
           insertProductPstmt.setString(2, &quot;테스트입출금통장&quot;);
           insertProductPstmt.setString(3, &quot;10&quot;);
           insertProductPstmt.executeUpdate();

           savepoint = conn.setSavepoint(&quot;AfterProductInsert&quot;);

           insertProductHistoryPstmt.setString(1, &quot;600&quot;);
           insertProductHistoryPstmt.setString(2, &quot;테스트입출금통장&quot;);
           insertProductHistoryPstmt.setString(3, &quot;10&quot;);
           insertProductHistoryPstmt.setString(4, &quot;I&quot;);
           insertProductHistoryPstmt.executeUpdate();
           
           throw new SQLException(&quot;고의로 에러 발생&quot;);

       } catch (SQLException e) {
           conn.rollback(savepoint);
           throw e;
       }

   } catch (SQLException e) {
       System.out.println(&quot;Database error: &quot; + e.getMessage());
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MySQL에서는 다음처럼 날아갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;647&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecStlv/dJMcaibjKC6/Brd71cC6h3Djv1cUqLxhz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecStlv/dJMcaibjKC6/Brd71cC6h3Djv1cUqLxhz1/img.png&quot; data-alt=&quot;아래에서 위로 보면 됩니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecStlv/dJMcaibjKC6/Brd71cC6h3Djv1cUqLxhz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecStlv%2FdJMcaibjKC6%2FBrd71cC6h3Djv1cUqLxhz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;183&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아래에서 위로 보면 됩니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;근데 마지막에 롤백이 실행된 것을 볼 수 있는데요, 이는 앞서 말한 것처럼 MySQL JDBC Driver는 Connection의 close가 호출됐을 때 Auto Commit 모드가 꺼져 있으면 롤백이 수행되게끔 구현되어있기 때문입니다. 그렇다면 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;어차피 트랜잭션을 닫을 때 전체가 롤백되는 거라면 SavePoint를 사용한 부분 롤백은 아무 의미 없는 거 아닌가?&quot;&lt;/b&gt;&lt;/span&gt; 라는 생각이 들었는데요. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 이것은 &lt;/span&gt;&lt;b&gt;SavePoint의 사용 패턴이 트랜잭션을 부분 롤백한 채로 끝내는 데 목적이 있는 게 아니라 부분 롤백 후 트랜잭션을 계속 진행 하는 데 목적이 있기 때문&lt;/b&gt;&lt;/span&gt;입니다. 예를 들자면 배치 작업 등에서 일부 실패를 허용할 때 등이 있겠고, 이런 상황을 코드로 간략하게 보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771851801565&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;conn.setAutoCommit(false);

작업A();
Savepoint sp = conn.setSavepoint(&quot;sp1&quot;);

try {
    작업B(); // 실패 가능성 있는 작업
} catch (SQLException e) {
    con.rollback(sp); // sp1까지만 롤백, 트랜잭션을 여기서 닫지 않음!
}

작업C(); // 계속 진행

conn.commit(); // A와 C가 반영됨 &amp;larr; 이걸 해야 의미가 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;참고) 스프링의 트랜잭션 전파 옵션이 내부적으로 SavePoint를 활용한다고 하는데요, 곧 스프링 트랜잭션에 대해서도 공부를 하며 이 부분을 좀 더 파볼려고 합니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1 -&amp;nbsp; 4. JDBC와 트랜잭션 격리 수준&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC API는 다음과 같은 5가지의 격리 수준을 지원합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TRANSACTION_NONE (0)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TRANSACTION_READ_UNCOMMITTED (1)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TRANSACTION_READ_COMMITTED (2)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TRANSACTION_REPEATABLE_READ (4)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TRANSACTION_SERIALIZABLE (8)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Connection의 setTransactionIsolation 메서드를 호출하여 특정한 격리수준을 현재 세션에 설정할 수 있으며, 현재 세션의 격리 수준은 getTransactionIsolation메서드로 조회할 수 있습니다. MySQL JDBC Driver는 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;setTransactionIsolation &lt;/span&gt;메서드 호출 시 DB로 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 등이 날라갑니다. 참고로 벤더사들이 구현한 JDBC Driver 별로 특정 격리수준에 대한 setting이 드라이버 레벨에서 불가능하게 만들어져있다든지, 아니면 DB별로 특정 트랜잭션 격리 수준으로 SET SESSION하는게 안 될 수도 있습니다. (예를 들면, MySQL에서는 TRANSACTION_NONE을 설정할 수 없습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고)&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 격리 수준의 의미와 발생 가능한 문제들을 다루는 것은 JDBC 범위를 넘어간다고 생각하지 않아 따로 이 글에선 작성하지 않으려고 하는데요, 혹시 궁금하신 분들은 제가 예전에 쓴 글을 보시면 좋을 것 같습니다 :)&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://jofestudio.tistory.com/145&quot;&gt;https://jofestudio.tistory.com/145&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 커넥션 풀&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&quot;&lt;a style=&quot;background-color: #f1f1f1; color: #444444; text-align: center;&quot; href=&quot;https://jofestudio.tistory.com/153&quot;&gt;[JDBC] (1편) MyBatis, JPA 없이 JDBC로만 DB를 다루는 세상이었다면&lt;/a&gt;&quot;에서 JDBC를 사용하는 애플리케이션은 DriverManager 또는 DataSource로부터 Connection을 얻는다고 소개했었는데요. 커넥션 풀링은 JDBC API에서는 DataSource에서 지원되는 기능으로 설계됐습니다. 즉 DataSource는 커넥션 풀링을 지원하냐 안 하냐에 따라 구분할 수 있습니다. 풀링을 지원하지 않는 DataSource는 getConnection을 통해 얻은 Connection이 DB와의 물리적인 연결을 의미하는 요소이고, close할 때는 실제로 물리적인 연결이 끊어지게 됩니다. 하지만 JDBC API는 풀링을 지원하는 DataSource는 내부 동작이 조금 달라지도록 설계했는데, 이를 이해하려면 JDBC API가 제공하는 다음 두 인터페이스에 대한 이해가 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start;&quot;&gt;2 - 1. ConnectionPoolDataSource&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;ConnectionPoolDataSource는 DataSource 인터페이스가 Connection타입의 객체를 반환하는 getConnection 메서드를 가졌던 것과는 다르게 , PooledConnection 타입의 객체를 만들어 반환하는 getPooledConnection이라는 메서드를 갖고 있는 인터페이스입니다. 즉 PooledConnection들의 팩토리 역할을 하는 객체로 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start;&quot;&gt;2 - 2. PooledConnection&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;DB와 맺는 실제 물리적인 연결을 래핑한 객체로, 커넥션풀이 저장되는 단위가 되는 커넥션이 PooledConnection입니다. 다시 말하면 커넥션풀을 통해 재사용되는 커넥션이 PooledConnection이라는 이야기입니다. 이 인터페이스는 getConnection이라는 메서드를 가지며, 이 메서드가 Connection 타입의 객체(DataSource의 getConnection을 하면 받는 그 타입)을 리턴합니다. 즉 PooledConnection과 Connection은 서로 다른 객체입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;아직까진 이해가 조금 힘들 수 있는데요, (왜냐하면 제가 그랬으니까..) 간략히 말하자면 커넥션 풀링을 지원하는 DataSource는 두 인터페이스들을 사용해서 다음처럼 동작합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ConnectionPoolDataSource를 통해 DB와의 물리적 연결을 의미하는 PooledConnection들을 만들어 풀에 저장해둠&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC 애플리케이션이 DataSource의 getConnection을 통해 커넥션 요청 (= DataSource API를 통해 커넥션 요청)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커넥션 풀 매니저(= DataSource의 구현체)는 커넥션풀에 있는 PooledConnection을 하나 골라, getConnection을 통해 JDBC Application이 사용할 수 있는 Connection을 만들어 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;그림으로 도식화하면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1306&quot; data-origin-height=&quot;1398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8cEeU/dJMcahwGWEU/KzVqmrb3XHuMBRbAVOiYQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8cEeU/dJMcahwGWEU/KzVqmrb3XHuMBRbAVOiYQk/img.png&quot; data-alt=&quot;JSR-221에서 캡처했습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8cEeU/dJMcahwGWEU/KzVqmrb3XHuMBRbAVOiYQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8cEeU%2FdJMcahwGWEU%2FKzVqmrb3XHuMBRbAVOiYQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;1398&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;1398&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JSR-221에서 캡처했습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;즉 커넥션 풀링을 지원하는 DataSource를 사용하는 경우, JDBC 애플리케이션에게 반환한 Connection은 논리적인 의미의 커넥션일 뿐 DB와의 실제 물리적 연결을 의미하진 않는 거고, ConnectionPoolDataSource가 만든 PooledConnection이 DB와의 실제 물리적 연결을 의미하는 셈입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;이로 인해 PooledConnection의 getConnection을 통해 얻은 Connection의 close 메서드가 호출될 때는 조금 다르게 동작하게 되는데요. 바로 물리적인 연결을 끊는 것이 아니라, 해당 Connection을 비활성화하고 연관된 PooledConnection을 다시 풀에 반납하는 식으로 동작하게 됩니다. 즉, 풀링을 지원하지 않는 DataSource든 풀링을 지원하는 DataSource든 똑같이 getConnection을 통해&amp;nbsp; Connection을 획득해서 사용할 수 있으나, 엄연히 말하면 이 둘은 타입은 같지만 서로 다른 구현체를 쓰는 것임을 알 수 있습니다. 또한 뻔하긴 합니다만 PooledConnection은 생애주기동안 여러 Connection을 만들게 됨도 알 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;참고 1) PooledConnection가 기존에 반환한 Connection이 활성 상태일 때 getConnection을 호출하면, 기존에 활성 상태로 있던 Connection이 비활성화됩니다. 이를 통해 강제로 클라이언트가 사용하는 커넥션을 끊을 수 있습니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;참고 2) PooledConnection의 close를 호출해야 실제 DB와의 물리적 연결을 끊게 됩니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;조금 더 세부적으로 파보면, &lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;PooledConnection에서 Connection을 만들어 줄 때 &lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;ConnectionEventListener라는 JDBC에서 제공하는 또다른 인터페이스의 구현체를 &lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;PooledConnection에 등록하는 과정이 있습니다. 이 인터페이스는 &lt;span style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot;&gt;connectionClosed 메서드와 &lt;span style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot;&gt;connectionErrorOccured라는 메서드를 가지는데요, PoolecConnection이 반환한 Connection이 close될 때 이 인터페이스의 &lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #141413; text-align: start;&quot;&gt;connectionClosed 메서드를 통해 &lt;span style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot;&gt;PooledConnection가 풀로 반환되게 됩니다.&lt;/span&gt;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #303030; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;⭐️&lt;/b&gt;&lt;/span&gt; 2 - 3. 하지만 실제로 이 설계들을 항상 지키진 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;방금 본 것처럼, JDBC는 ConnectionPoolDataSource, PooledConnection을 사용해서 커넥션 풀링을 사용하도록 설계했습니다. 하지만 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;실제 커넥션풀 라이브러리로 사용되는 HikariCP나 Apache Commons DBCP 등은 이 표준을 지키지 않고 구현한 경우가 많습니다&lt;/b&gt;&lt;/span&gt;. DataSource에서 getConnection을 해서 Connection을 얻는 건 똑같고 커넥션 풀링이 지원되는 건 똑같지만, 내부적으로는 ConnectionPoolDataSource나 PooledConnection을 쓰지 않고 본인들만의 방식으로 풀링을 구현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;왜 그랬을까요? 표준은 지켜야 하는게 아니었을까요!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 HikariCP나 Apache Commons DBCP 개발자가 아닌 이상 그 이유를 우리가 100% 명확히 알 수는 없습니다만, 그래도 깃허브 이슈나 남아있는 문서 등을 통해 그 분들이 왜 표준을 지키지 않고 독자적인 방식으로 구현하는 방향으로 기술적인 의사결정을 했는지 유추해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;깃허브 HikariCP 레포지토리에서 다음 위키문서와 이슈를 찾아볼 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/issues/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/brettwooldridge/HikariCP/issues/54&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특히나 이슈의 내용이 흥미로운데요! 요약하면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;dimzon(이슈 작성자) :&lt;/b&gt;&lt;/span&gt; HikariCP가&amp;nbsp;ConnectionPoolDataSource를 지원하도록 구조를 개선해보는게 어때?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;brettwooldridge&lt;/b&gt; &lt;/span&gt;: PooledConnection이 좋은 의도로 만들어진 인터페이스긴 하지만 풀링에 필요한 풍부한 기능은 없어. 예컨대 HikariCP는 자체적으로 Connection의 프록시를 제공하는데, 얘는 setTransactionIsolationLevel 등이 호출되면 가로채서 상태 변화도 추적한 뒤, 커넥션이 반납될 때 상태 변화가 없었다면 리셋하지 않아. 또 Postgre JDBC Driver의 ConnectionPoolDataSource 구현을 확인해보니, PooledConnection이 예상치 않게 동작해. JDBC Driver의 ConnectionPoolDataSource 구현이 우리가 제공하는 고성능 풀링을 위한 설정들이 제대로 적용되어있어야 할텐데, 그렇지가 않아..&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #282a2c; text-align: start;&quot;&gt;커넥션풀 라이브러리를 구현하는 입장에서, JDBC 스펙대로 ConnectionPoolDataSource와 PooledConnection을 사용해서 풀링을 구현하려면 벤더사들이 제공하는 JDBC Driver이 만드는 구현체들에 의존할 수밖에 없는데, 그렇게 되면 어떤 드라이버를 쓰느냐에 따라 성능이 뒤죽박죽이 될 수도 있고 예상치 못한 동작이 가능할 수도 있습니다. 따라서 스스로 독자적으로 풀링을 구축하는 것이 더 안전하고 빠르다. 라고 생각한 듯 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #303030; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #282a2c; text-align: start;&quot;&gt;하지만 해당 이슈에 24년도에도 &lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #282a2c; text-align: start;&quot;&gt;ConnectionPoolDataSource 지원을 제고하자고 말한 내용이 있는 걸 보니.. 역시 개발과 기술에 정답은 없는 것 같습니다. 항상 트레이드 오프가 있고, 어떤 선택을 어떤 상황에서 어떤 근거로 했는지가 중요한 것 같네요.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2 - 4. 커넥션 풀 관련 속성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;JDBC API는 커넥션 풀에 관련하여 다음 속성들을 정의하고 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;maxStatements : 풀이 캐싱하는 Statement의 수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;initialPoolSize : 풀이 처음 만들어질 때 가질 커넥션 수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;minPoolSize : 풀이 항상 '사용 가능'해야 하는 최소 커넥션의 수. 0개면 필요할 때마다 만들겠다는 의미&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;maxPoolSize : 풀이 가질 수 있는 최대 커넥션 수, 0이면 제한없다는 의미&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;maxIdleTime : 커넥션이 닫히기 전, 물리적 연결이 사용되지 않는 채로 남아있어야 하는 시간(초)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;propertyCycle : 커넥션풀 설정을 반영하기 위한 간격(초)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커넥션풀 공급업체는 스스로만의 속성들을 사용 가능하나, 그러면 위 이름이 아닌 다른 속성들을 사용해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;레퍼런스&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JSR-221 (&lt;a href=&quot;https://download.oracle.com/otndocs/jcp/jdbc-4_3-mrel3-spec/&quot;&gt;https://download.oracle.com/otndocs/jcp/jdbc-4_3-mrel3-spec/&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/jdbc/basics/sqldatasources.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/tutorial/jdbc/basics/sqldatasources.html&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/javax/sql/ConnectionPoolDataSource.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/8/docs/api/javax/sql/ConnectionPoolDataSource.html&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/javax/sql/PooledConnection.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/8/docs/api/javax/sql/PooledConnection.html&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/javax/sql/DataSource.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/javase/8/docs/api/javax/sql/DataSource.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/JAVA</category>
      <category>ConnectionPoolDataSource</category>
      <category>JDBC</category>
      <category>PooledConnection</category>
      <category>자바</category>
      <category>커넥션 풀</category>
      <category>커넥션풀</category>
      <category>트랜잭션</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/154</guid>
      <comments>https://jofestudio.tistory.com/154#entry154comment</comments>
      <pubDate>Mon, 23 Feb 2026 23:36:39 +0900</pubDate>
    </item>
    <item>
      <title>[JDBC] (1편) MyBatis, JPA 없이 JDBC로만 DB를 다루는 세상이었다면</title>
      <link>https://jofestudio.tistory.com/153</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;자바는 DB를 다룰 때 각 DB별로 API를 만들지 않고 어떤 DB든 간에 JDBC라는 표준을 통해 접근할 수 있도록 인터페이스를 제공하고 있습니다. 아무래도 저는 학창 시절에는 JPA를 잠깐 사용하고, 현재 회사에서는 MyBatis를 사용하면서 DB 접근 기술의 근간에 위치하고 있는 JDBC만을 사용해서 DB를 다뤄본 적은 없는데요. 자바와 스프링 환경에 대한 공부를 하면서 JPA나 MyBatis가 추상화하고 있는 '내부적인 동작 방식'이 궁금해져서 공부를 했었고, 그 내용을 공유하고자 합니다.&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;먼저 JDBC가 어떤 계층 구조를 가지고 애플리케이션과 DB 사이를 연결하는지에 대한 구조도입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2786&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csJ9IA/dJMcaiILIMX/r2NnPdDcDEJB6nOkQKdLgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csJ9IA/dJMcaiILIMX/r2NnPdDcDEJB6nOkQKdLgK/img.png&quot; data-alt=&quot;JDBC 아키텍처 구조도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csJ9IA/dJMcaiILIMX/r2NnPdDcDEJB6nOkQKdLgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsJ9IA%2FdJMcaiILIMX%2Fr2NnPdDcDEJB6nOkQKdLgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2786&quot; height=&quot;1226&quot; data-origin-width=&quot;2786&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JDBC 아키텍처 구조도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우리가 비즈니스 로직을 짜는 애플리케이션 레이어에서 JDBC API를 호출해서 DB에 접근하게 되는데요. 이렇게&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JDBC를 통해 DB를 접근하게 되면 다음 절차를 따르게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) 커넥션 생성&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) 구문(Statement) 생성 및 컴파일&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3) 구문 실행&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4) 실행 결과 처리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5) 커넥션 해제&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 각 절차들을 조금 더 자세히 알아보고, JDBC를 통해 DB를 다뤄보는 예시를 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고) 공식 문서 상에서 커넥션을 맺게 되는 target DB를 'data source'라고 표현하는데요. 아무래도 자바 표준 스펙에서 정의하고 있고 이 글에서도 자주 쓰이는 'DataSource'와 헷갈릴 소지가 있어 이 글에서는 'data source'를 '데이터 원천'으로 작성했습니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 커넥션 생성 (Establishing Connection)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커넥션 생성은 특정한 데이터 원천과 커넥션(연결)을 맺는 것을 의미합니다. 데이터 원천은 DBMS가 될 수도 파일시스템이 될 수도 있습니다. 커넥션은 JDBC 인터페이스에서 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Connection 객체로 표현되는데요, &lt;/span&gt;일반적으로 JDBC를 사용하는 애플리케이션은 커넥션 생성 시 다음 두 클래스 중 하나를 사용하여 커넥션을 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) DriverManager&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여러 JDBC Driver들을 등록하고 관리하는 역할을 하는 매니저 클래스입니다. DriverManager를 사용해 커넥션을 얻을 경우, 데이터베이스 URL을 사용해 애플리케이션을 데이터 원천에 연결합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) DataSource&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DriverManager보다 개선된 기능(커넥션 풀링 등)을 제공하는 인터페이스입니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;벤더사(MySQL, Oracle 등)마다 DataSource 구현체 클래스를 제공하며, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;DataSource 객체의 속성(properties)들은 특정한 데이터 원천을 나타내도록 설정됩니다. DataSource 사용 시 아무래도&amp;nbsp; 애플리케이션 입장에서는 인터페이스에 의존하게 되는 것이므로, 내부적인 연결 정보(properties 등으로 표현되는)나 구현체가 바뀌어도 영향을 받지 않게 된다는 장점이 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;참고) 커넥션 풀링 : DB Connection 객체를 미리 생성해 풀에 보관하고, 필요할 때마다 빌려 쓴 뒤 반납하는 관리 기법을 말합니다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1 -&amp;nbsp; 1.DriverManager를 통한 커넥션 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고) dbms는 mysql을 쓴다고 가정합니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Connection getConnectionByDriverManager(String username, String password) throws SQLException {
    Properties connectionProps = new Properties();
    connectionProps.put(&quot;user&quot;, username);
    connectionProps.put(&quot;password&quot;, password);

    Connection conn = DriverManager.getConnection(
            &quot;jdbc:mysql://localhost:3306/bank_demo&quot;,
            connectionProps
    );
    
    System.out.println(&quot;Connected to database successfully.&quot;);
    return conn;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DriverManager의 getConnection 메서드가 커넥션을 생성하며, 코드에 드러나있는 것처럼 데이터베이스URL을 사용하여 커넥션을 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1 - 2. DataSource를 통한 커넥션 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 말했듯 DataSource는 인터페이스로, 벤더사마다 DataSource 인터페이스의 구현체 클래스를 제공합니다. 보통 시스템 관리자가 Tomcat 등을 사용해서 DataSource를 배포(등록)한 뒤 애플리케이션 개발자가 등록된 DataSource를 사용해 커넥션을 얻는 식으로 사용합니다. 참고로 DataSource는 커넥션 풀링과 분산 트랜잭션을 제공할 수 있다는 장점도 가집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DataSource는 다음 3가지 방식으로 구현될 수 있습니다. (1번 선택지는 무조건 포함해야 하고, 대개 분산 트랜잭션을 지원하는 DataSource는 커넥션 풀링도 지원합니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) 풀링되지 않거나 분산 트랜잭션에서 쓰이지 않는 심플한 커넥션을 제공하는 Basic한 DataSource&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) 커넥션 풀링을 지원하는 DataSource (즉 여기서 쓰이는 Connection 객체는 재사용될 수 있는 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Connection &lt;/span&gt;객체)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3) 분산 트랜잭션을 지원하는 DataSource (즉 여기서 쓰이는 Connection 객체는 분산 트랜잭션에서 사용될 수 있는 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Connection &lt;/span&gt;객체)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1 - 2 - 1. DataSource 객체 배포(등록)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시스템 관리자가 애플리케이션 개발자들이 사용 가능하도록 DataSource 객체를 배포(등록)하는 과정이며, 보통 다음과 같은 형식의 3단계로 구성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) DataSource 객체 생성&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) 해당 객체의 properties 세팅&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3) JNDI(Java Naming and Directory Interface) API 등의 방법을 통해 해당 객체를 등록&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;참고) 과거 WAS 환경에서는 이러한 DataSource를 JNDI라는 디렉토리 서비스에 등록해두고 찾아 쓰는 방식을 주로 사용했습니다. 현대 스프링 기반 환경에서는 빈으로 등록해서 많이 활용합니다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;MysqlDataSource dataSource = new MysqlDataSource();

dataSource.setServerName(&quot;localhost&quot;);
dataSource.setPortNumber(3306);
dataSource.setDatabaseName(&quot;bank_demo&quot;);
dataSource.setUser(username);
dataSource.setPassword(password);

Context ctx = new InitialContext();
ctx.bind(&quot;jdbc/BankDemoDS&quot;, dataSource);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1 - 2 - 2. DataSource 객체 사용 (애플리케이션 개발자 역할)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;애플리케이션 개발자는 시스템 관리자가 등록한 DataSource를 다음과 같이 활용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) JNDI를 통해 DataSource 조회 (lookup)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) 반환된 DataSource의 getConnection() 메서드를 호출하여 커넥션 획득&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3) 커넥션 풀링 사용 시 커넥션을 다 쓰고 나면 커넥션 반환 (close 호출)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Context ctx = new InitialContext();

DataSource dataSource = (DataSource) ctx.lookup(&quot;jdbc/BankDemoDS&quot;);

Connection conn = dataSource.getConnection();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 쿼리문 생성 및 작성 (Create Statements)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;JDBC 인터페이스에서 쿼리문(구문)을 표현하는 인터페이스는 Statement입니다. 해당 객체를 '실행'하게 되면 실행결과로 ResultSet 객체(데이터베이스 결과 집합을 의미하는 JDBC 인터페이스 상에서의 객체)를 만들어줍니다. Statement 객체를 만들려면 Connection 객체가 필요하며 생성 예시는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;stmt = con.createStatement();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;Statement에는 다음 3종류가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1) Statement&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;파라미터가 없는 간단한 SQL문 생성에 쓰입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2) PreparedStatement&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;Statement를 상속하며, 파라미터가 존재하는 SQL문을 미리 컴파일하는데 쓰입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;3) CallableStatement&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;PreparedStatement를 상속하며, 파라미터가 존재할 수 있는 프로시저들을 실행할 때 쓰입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2 - 1. PreparedStatement&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Statement를 상속하는 클래스로, PreparedStatement의 주요 특징은 Statement와는 다르게 생성될 때 SQL문을 받는다는 점입니다. 이렇게 되면 대부분 해당 SQL문이 DBMS로 즉시 보내져 컴파일되는데요, 이를 통해 PreparedStatement가 실행되면 DBMS는 컴파일 과정 없이 SQL문을 바로 실행할 수 있게 된다는 장점을 얻을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;참고)&amp;nbsp;&lt;/i&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;&lt;i&gt;JDBC는 JVM과 DBMS 사이의 연결을 표준화한 스펙인 거고,&amp;nbsp;&lt;/i&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;&lt;i&gt;미리 컴파일된 쿼리를 저장하는 건 JDBC 쪽이 아니라 MySQL 등이 PREPARE 등을 통해 제공하는 기능입니다. 저희 회사에서 쓰고 있는 SingleStore의 경우는 PREPARE 기능을 제공하지 않는데, 이런 경우는 클라이언트(JDBC 쪽) 수준에서 최적화를 진행합니다. 또한 JDBC Driver 별로 특정 설정들을 통해 DB의 PREPARE를 사용할지 안 할지도 고를 수 있습니다. 이렇게 DB의 기능을 활용하는 PreparedStatement를 ServerPreparedStatement, 클라이언트 수준에서 최적화하는 PreparedStatement를 ClientPreparedStatement라고 부릅니다.&lt;/i&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;아래는 MySQL의 PREPARE를 사용하는 예제이며, 이 글 하단 레퍼런스 탭에 있는 카카오페이 테크블로그를 참고했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768133351295&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- prepared statement 생성
PREPARE pstmt FROM 'SELECT * FROM TB_CUSTOMER WHERE id = ?';

-- 파라미터를 지정하여 prepared statement 실행
SET @a = 1;
EXECUTE pstmt USING @a;

-- prepared statement 제거
DEALLOCATE PREPARE pstmt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PreparedStatement는 파라미터가 없는 SQL문의 실행에도 쓰일 수 있지만, 대개 파라미터가 있는 SQL문의 실행에 자주 쓰입니다. 이때 PreparedStatement는 클라이언트가 제공한 데이터를 SQL문장의 일부로서가 아니라 파라미터로만 취급하기 때문에 악의적인 입력이 코드로 해석되지 않아서, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;SQL Injection을 자연스럽게 예방하는 효과도 얻을 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;참고) 조금 더 풀어서 설명해보면,&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; PreparedStatement는 SQL문 생성시 파라미터가 들어가는 위치들은 ?(Question Mark)를 두고, 이 구조를 컴파일한 뒤 나중에 ?가 있던 위치에 setter메서드를 통해 파라미터들을 세팅하게 되는데요. 이때 파라미터 값들을 코드가 아니라 '단순한 문자열 값'이나 숫자 데이터로만 처리하게 되어 SQL Injection이 예방되는 효과를 얻게 됩니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PreparedStatement의 생성과 파라미터 세팅, 실행 예시는 다음과 같습니다. SQL문에서 파라미터가 들어갈 곳은 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;?(Question Mark)&lt;/span&gt;로 표시하며, 파라미터 설정은 PreparedStatement의 setter 메서드 중 적절한 것을 골라 사용합니다. setter 메서드의 파라미터로는 인덱스 번호와 설정할 값을 넘기며 인덱스는 1부터 시작합니다. 참고로 한 번 설정된 파라미터는 해당 인덱스 번호에 대한 setter 메서드를 호출하거나, PreparedStatement의 clearParameters()를 호출하기 전에는 설정된 값을 유지합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;String updateSql = &quot;UPDATE account SET balance = balance + ? WHERE id = ?&quot;;
PreparedStatement pstmt = conn.prepareStatement(updateSql);

pstmt.setLong(1, 10000L);
pstmt.setString(2, &quot;00000001029301&quot;);
        
pstmt.executeUpdate();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;3. 쿼리 실행 (Executing Queries)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;Statement 객체에서 execute 계열 메서드를 호출하여 쿼리를 실행 가능하며,&amp;nbsp; execute 계열 메서드는 다음 3종류가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1) execute&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;두 개 이상의 ResultSet 객체를 리턴하는 SELECT 수행 시 사용하며, 쿼리가 반환하는 첫 번째 결과가 ResultSet객체면 true를 리턴합니다. Statement객체의 getResultSet 메서드를 반복 호출하여 ResultSet객체들을 하나하나 가져올 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2) executeQuery&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;하나의 ResultSet 객체를 리턴하는 SELECT 수행 시 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;3) executeUpdate&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;DML 수행 시 사용하며, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;작성된 쿼리를 통해 영향을 받은 row의 수를 리턴합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;예를 들면 다음처럼 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;ResultSet rs = stmt.executeQuery(query);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 실행 결과 처리 (Processing ResultSet Objects)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4 - 1. ResultSet?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Statement를 실행하여 ResultSet을 얻었다면, Cursor를 통해 ResultSet 객체의 데이터들에 접근할 수 있습니다. ResultSet은&amp;nbsp;&lt;/span&gt;데이터베이스 결과 집합을 나타내는 데이터 테이블로, 일반적으로 SELECT문을 실행하여 생성됩니다. PreparedStatement 등 Statement 인터페이스를 구현하는 모든 객체를 통해 생성 가능하며, 실행된 쿼리 결과를 조작하고 검색하는 다양한 메서드를 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Cursor를 통해 ResultSet 객체의 데이터들에 접근할 수 있는데요, 여기서의 Cursor는 DB Cursor의 개념이 아니라, ResultSet 객체의 데이터 하나하나를 가리키는 포인터를 말합니다. 최초에 Cursor는 첫 번째 row 앞에 위치하고 있고, ResultSet객체가 가지는 여러 메서드들을 통해 Cursor가 가리키는 위치(= 가리키는 row)를 바꿀 수 있습니다. 대표적으로 ResultSet의 next메서드는 Cursor를 다음 행으로 이동시키는데, Cursor가 마지막 행 뒤에 위치하게 되면 false를 리턴하는 점을 활용해 while루프를 통하여 ResultSet의 모든 데이터를 순회할 수 있습니다.&amp;nbsp; 그 외에&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: left;&quot;&gt;next말고도 previos, first 등등 Cursor를 움직일 수 있는 방법은 다양합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;4 - 2. ResultSet에서 행의 특정 컬럼값 가져오기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;ResultSet은 현재 Cursor가 가리키는 행에서 원하는 컬럼값을 가져올 수 있는 getter메서드들을 제공합니다. 컬럼의 인덱스 번호(1부터 시작)를 사용하거나 컬럼이름을 사용하여 값을 가져올 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;String selectSql = &quot;SELECT * FROM TB_CUSTOMER WHERE CUST_ID = ?&quot;;
PreparedStatement pstmt = conn.prepareStatement(selectSql);

pstmt.setLong(1, 1L);
ResultSet rs = pstmt.executeQuery();

while (rs.next()) {
    Long custId = rs.getLong(1); // CUST_ID. 1은 컬럼의 인덱스
    String custName = rs.getString(&quot;CUST_NAME&quot;); // CUST_NAME

    System.out.println(&quot;custId: &quot; + custId + &quot;, custName: &quot; + custName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. 커넥션 해제 (반납)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Connection객체와 Statement객체, ResultSet객체들을 다 사용했다면 close메서드를 통해 리소스들을 해제합니다. 자바에서 제공하는 try-with-resources 문법을 사용하면 try 블록을 벗어날 때 자동으로 자원 해제를 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예를 들면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768115709848&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 account SET balance = balance + ? WHERE id = ?&quot;;
    
try (Connection conn = dataSource.getConnection();
    PreparedStatement pstmt = conn.prepareStatement(sql)) {
        
    pstmt.setLong(1, amount);
    pstmt.setString(2, accountId);
    pstmt.executeUpdate();
        
} catch (SQLException e) {
    // 예외 처리 로직
    log.error(&quot;Database error&quot;, e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;참고) SELECT 쿼리일 경우 ResultSet도 해제해야 합니다.&amp;nbsp;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;JDBC만을 사용한 DB 접근 실습&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 JDBC만을 사용해 DB에 접근하는 것을 간단하게 실습해봤습니다. 실습 환경은 다음과 같았습니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) Java 21&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) mysql-connector-j 9.3.0&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3) Mysql 8.0.33&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테이블은 현재 제가 개인적으로 하는 토이 프로젝트에서 쓰고 있는 테이블 중 다음 테이블에 대해 SELECT하는 것으로 해봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;802&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k3LCZ/dJMcadHsEO6/K6Qp6B44YE1EQY20hHzP60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k3LCZ/dJMcadHsEO6/K6Qp6B44YE1EQY20hHzP60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k3LCZ/dJMcadHsEO6/K6Qp6B44YE1EQY20hHzP60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk3LCZ%2FdJMcadHsEO6%2FK6Qp6B44YE1EQY20hHzP60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;419&quot; height=&quot;229&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static List&amp;lt;Customer&amp;gt; getCustomers() {
    List&amp;lt;Customer&amp;gt; customers = new ArrayList&amp;lt;&amp;gt;();
    String selectSql = &quot;SELECT CUST_ID, CUST_NAME, RES_ID, CUST_TYPE FROM TB_CUSTOMER LIMIT ?&quot;;

    try (Connection conn = JdbcSample.getConnectionByDriverManager();
         PreparedStatement pstmt = conn.prepareStatement(selectSql)) {

        pstmt.setInt(1, 10);

        try (ResultSet rs = pstmt.executeQuery()) {
            while (rs.next()) {
                Long custId = rs.getLong(&quot;CUST_ID&quot;);
                String custName = rs.getString(&quot;CUST_NAME&quot;);
                String resId = rs.getString(&quot;RES_ID&quot;);
                String custType = rs.getString(&quot;CUST_TYPE&quot;);

                customers.add(new Customer(custId, custName, resId, custType));
            }
        }

    } catch (SQLException e) {
        System.out.println(&quot;Database error: &quot; + e.getMessage());
    }

    return customers;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으론, PreparedStatement나 ResultSet 등 MyBatis나 JPA가 추상화해주고 있는 영역을 직접 공부도 해보고 처리도 해보니 애플리케이션과 DB가 어떻게 대화하고 있는지(?) 좀 더 잘 이해하게 된 것 같습니다. 역시 해보길 잘했다는 생각입니다. 다만 간단한 쿼리를 처리하고 있음에도 불구하고 리소스의 해제를 신경써야 한다거나 파라미터 세팅을 자꾸 해줘야 한다는 점, ResultSet을 매번 꺼내서 내가 사용할 객체의 값으로 세팅해줘야 하는 점들이 &quot;아 이거 반복되면 번거롭겠다&quot;라는게 느껴졌습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이를 토대로 MyBatis와 JPA가 어떤 것들을 어떻게 추상화했는지 등도 공부할 수 있겠다는 생각이 듭니다. 또한 커넥션풀은 어떻게 활용되고 있는지에 대한 궁금증도 생기고 있습니다. 그래서 여기서 멈추지 않고, 커넥션풀을 사용하는 DataSource는 커넥션풀을 어떻게 활용하고 있는건지, 그리고 MyBatis와 JPA가 어떤 점들을 해결하려고 했고 어떤 것들을 포기한 기술인지도 공부를 해서 블로그에 올려보겠습니다!&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;글이 잘 읽힐 지 모르겠네요. 그래도 끝까지 봐주셔서 감사합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;레퍼런스&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768119987961&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignRight&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Lesson: JDBC Basics (The Java&amp;trade; Tutorials &amp;gt; JDBC Database Access)&quot; data-og-description=&quot;The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Dev.java for updated tutorials taking advantag&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Lesson: JDBC Basics (The Java&amp;trade; Tutorials &amp;gt; JDBC Database Access)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Dev.java for updated tutorials taking advantag&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/how-preparedstatement-works-in-our-apps/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tech.kakaopay.com/post/how-preparedstatement-works-in-our-apps/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768124665888&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignRight&quot; data-og-type=&quot;article&quot; data-og-title=&quot;우리의 애플리케이션에서 PreparedStatement는 어떻게 동작하고 있는가 | 카카오페이 기술 블로그&quot; data-og-description=&quot;JDBC를 직접 핸들링하지 않고, 다양한 추상화 계층 위에서 동작하는 우리의 애플리케이션에서 PreparedStatement는 어떻게 동작하는지 탐구한 결과를 공유합니다.&quot; data-og-host=&quot;tech.kakaopay.com&quot; data-og-source-url=&quot;https://tech.kakaopay.com/post/how-preparedstatement-works-in-our-apps/&quot; data-og-url=&quot;https://tech.kakaopay.com/post/how-preparedstatement-works-in-our-apps/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqBM3M/hyZRkxfaD7/KRbzv2SxMZesAMe70dAC80/img.png?width=480&amp;amp;height=319&amp;amp;face=0_0_480_319,https://scrap.kakaocdn.net/dn/LLj1N/hyZReqhj65/gkFyedXlglQkO12lDOVSR0/img.png?width=480&amp;amp;height=319&amp;amp;face=0_0_480_319&quot;&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/how-preparedstatement-works-in-our-apps/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.kakaopay.com/post/how-preparedstatement-works-in-our-apps/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqBM3M/hyZRkxfaD7/KRbzv2SxMZesAMe70dAC80/img.png?width=480&amp;amp;height=319&amp;amp;face=0_0_480_319,https://scrap.kakaocdn.net/dn/LLj1N/hyZReqhj65/gkFyedXlglQkO12lDOVSR0/img.png?width=480&amp;amp;height=319&amp;amp;face=0_0_480_319');&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;우리의 애플리케이션에서 PreparedStatement는 어떻게 동작하고 있는가 | 카카오페이 기술 블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JDBC를 직접 핸들링하지 않고, 다양한 추상화 계층 위에서 동작하는 우리의 애플리케이션에서 PreparedStatement는 어떻게 동작하는지 탐구한 결과를 공유합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.kakaopay.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/JAVA</category>
      <category>DB</category>
      <category>db접근</category>
      <category>java</category>
      <category>JDBC</category>
      <category>JPA</category>
      <category>MyBatis</category>
      <category>PreparedStatement</category>
      <category>resultset</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/153</guid>
      <comments>https://jofestudio.tistory.com/153#entry153comment</comments>
      <pubDate>Sun, 11 Jan 2026 21:48:42 +0900</pubDate>
    </item>
    <item>
      <title>[회고] 트랜잭션 아웃박스 패턴, 과연 정답이었을까?</title>
      <link>https://jofestudio.tistory.com/152</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;당시 개발했던 기능 소개&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2025년 올해, 회사에서 했던 개발 업무 중 가장 기억에 남는 경험을 꼽으라면 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;결재문서&amp;nbsp;&lt;/b&gt;&lt;b&gt;데이터를 연동하고 알림을 발송하는 기능에 트랜잭션 아웃박스 패턴을 적용한 것&lt;/b&gt;&lt;/span&gt;입니다. 회사 내에서 기안되는 결재문서 중에는 CEO까지 결재해야 하는 문서들이 있는데요. 해당 문서들에 비리나 이상, 위법성 등이 없는지 감사팀에서 매번 감사를 해왔는데, 제가 운영하던 감사시스템에 이렇게 CEO까지 결재하는 문서들을 연동해서 새로운 감사 대상 문서가 기안되면 감사팀에게 알림을 보내고, 결재가 진행되다가 반려가 되면 반려 알림 등을 보내도록 하는 기능입니다. &quot;일상감사 기능&quot;이라고도 불렀고, CEO가 결재하는 문서들을 감사하는 만큼 비즈니스 중요도가 매우 높은 업무였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 연동 방법에 대한 고민&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;먼저 데이터를 연동하는 방법부터 고민을 했었습니다. 처음에는 감사시스템에 데이터 수신용 API를 만들고 전자결재시스템에서 새로운 문서가 기안되거나 기안된 문서의 결재가 진행될 때마다 제가 만든 API를 호출하게 해서 결재문서 데이터들을 공급받을까 했지만, 네트워크 오류 등으로 API 호출 등이 실패했을 때 재시도 전략을 어떻게 할 건지 등이 고민됐습니다. 예를 들어 API 호출에 실패해서 재시도해도 그 재시도가 또 실패하면? 등등.. 결국에는 API 호출을 통한 방법으로는 결재문서 데이터가 누락될 수 있다는 점에 보였는데, 이 &quot;일상감사 기능&quot;은 앞서 설명했듯이 비즈니스 중요도가 매우 높아서 데이터가 누락되는 일은 최대한 지양해야 했습니다. 따라서 다른 방법들을 알아봤는데요, 사내에 이기종 DB 간의 데이터 교환을 특정 주기로 해주는 솔루션(통상 EAI 라고 부르는)이 있어서 이를 활용하기로 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1746&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b42nwo/dJMcahiEIvO/IhXe3v4Eu8IGaXTThrB59K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b42nwo/dJMcahiEIvO/IhXe3v4Eu8IGaXTThrB59K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b42nwo/dJMcahiEIvO/IhXe3v4Eu8IGaXTThrB59K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb42nwo%2FdJMcahiEIvO%2FIhXe3v4Eu8IGaXTThrB59K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;740&quot; height=&quot;198&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결재문서시스템 DB에서 연동해야 하는 데이터들(처리여부 == N)을 조회하고, 이 데이터들을 감사시스템 DB에 적재하고, 결재문서시스템 DB에서 조회했던 데이터들의 처리여부를 갱신(N &amp;rarr; Y)합니다. 이 작업을 10분마다 수행하도록 스케쥴해주면, 오류 등이 발생해서 데이터 연동에 실패해도 자연스럽게 10분마다 재시도될 수 있도록 설정할 수 있었습니다. (결과적 정합성이 보장됨) 물론 이렇게 하면 결재문서에 상태변화가 생겨도 즉각적으로 감사시스템에 연동되는 건 아니지만, 감사팀 분들과 소통하며 완전한 실시간성의 필요성보다는 누락없이 안정적으로 데이터를 연동하는 것이 더 중요함을 도출했었기 때문에 이 방법을 통해 데이터를 연동하기로 했었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;※CEO까지 결재하는 문서들은 하루에 많으면 2개 정도 기안되는 상황이라, Kafka같은 컴포넌트의 사용은 연동 대상 데이터 양에 비해 과한 방법이라는 생각이 들어 고려하지 않았습니다&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;알림을 보내는 방법에 대한 고민&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이후에는 알림을 보내는 방법을 고민했습니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사내 표준 상 EAI 솔루션으로 주고받는 데이터들은 실제 감사 업무에서 사용하는 테이블에 연동하는게 아니라 인터페이스 용도로 만든 테이블에 둬야 했기 때문에, 이 인터페이스 테이블에 있는 데이터들을 업무 테이블로 연동하고 알림을 보내야 했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 알림을 보내는 것에 대해서 처음에는 연동 후에 알림 보내지 뭐.. 라는 생각이었습니다. 하지만 개발 과정에서 대표적으로 다음과 같은 엣지 케이스가 발생 가능함을 알게 됐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 결재문서가 기안되고 감사시스템에 연동됨&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 감사팀에서 확인 후 해당 결재문서를 이상없음으로 처리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 이후 해당 결재문서의 결재가 진행되다가 중간에 반려되고, 문서 내용이 수정된 채로 재기안됨&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 하지만 반려 알림이나 재기안 알림이 발송되지 않음&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. 감사팀 입장에서는 이전에 이상 없다고 처리한 문서의 내용이 바뀌어 있게 됨&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 기능이 비즈니스 중요도가 높은 업무였던 만큼, 위와 같은 상황이 펼쳐지면 감사 업무에 굉장한 리스크가 될 수 있다는 생각이 들었습니다. 따라서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;결재문서 데이터 연동과 알림 발송이라는 두 작업의 원자성을 보장해줘야 한다&lt;/b&gt;&lt;/span&gt;는 생각이 들었고, 어떻게 해야 데이터가 연동된 후 알림이 어떤 상황이든 발송될 수 있도록 하지? 를 고민했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음에는 다음과 같이 데이터 연동에 성공하면 그 결과에 따라 알림을 발송하는 방법을 떠올렸는데요.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;2064&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y6aaD/dJMcacBELGF/9sYzGkVIRyguTexnRpp721/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y6aaD/dJMcacBELGF/9sYzGkVIRyguTexnRpp721/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y6aaD/dJMcacBELGF/9sYzGkVIRyguTexnRpp721/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY6aaD%2FdJMcacBELGF%2F9sYzGkVIRyguTexnRpp721%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2064&quot; height=&quot;996&quot; data-origin-width=&quot;2064&quot; data-origin-height=&quot;996&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하면 알림 발송이 실패할 수도 있기 때문에, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;데이터는 연동됐지만 알림 발송에 실패하는 케이스&lt;/b&gt;&lt;/span&gt;가 발생할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;2110&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAxSlS/dJMcaa4TNlS/KuD5s7wYYy7BZE35ju4NXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAxSlS/dJMcaa4TNlS/KuD5s7wYYy7BZE35ju4NXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAxSlS/dJMcaa4TNlS/KuD5s7wYYy7BZE35ju4NXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAxSlS%2FdJMcaa4TNlS%2FKuD5s7wYYy7BZE35ju4NXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2110&quot; height=&quot;1032&quot; data-origin-width=&quot;2110&quot; data-origin-height=&quot;1032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게하면 데이터 연동이 실패할 수도 있기 때문에 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;알림은 발송됐지만 데이터 연동은 실패하는 케이스&lt;/b&gt;&lt;/span&gt;가 발생할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;세 번째로는 데이터 연동 트랜잭션 내에서 알림 발송 결과에 따라 트랜잭션을 커밋 or 롤백하는 방법을 떠올렸는데요.&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;2050&quot; data-origin-height=&quot;1042&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqyOux/dJMcahbSVEo/V6OsQc7tuu46VAmO7KRkGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqyOux/dJMcahbSVEo/V6OsQc7tuu46VAmO7KRkGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqyOux/dJMcahbSVEo/V6OsQc7tuu46VAmO7KRkGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqyOux%2FdJMcahbSVEo%2FV6OsQc7tuu46VAmO7KRkGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2050&quot; height=&quot;1042&quot; data-origin-width=&quot;2050&quot; data-origin-height=&quot;1042&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마찬가지로 &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;알림 발송엔 성공했지만 데이터 연동은 실패하는 케이스&lt;/b&gt;&lt;/span&gt;가 발생할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 데이터 연동과 알림 발송이라는 두 작업의 원자성을 하나의 DB 트랜잭션으로 정확히 보장해주는 건 매우 힘들었습니다. 2 phase commit 같은 분산 트랜잭션도 알아봤지만 연동해야 하는 데이터 양에 비해 오버 엔지니어링이 될 거라는 생각이 들었는데요. 찾아보니 최소 1번은 이벤트를 전달하는 방법으로 트랜잭션 아웃박스 패턴(트랜잭셔널 아웃박스 패턴)이 있었고, 이를 활용해 다음처럼 설계할 수 있겠다는 생각에 도입을 하게 됐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1048&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fll3q/dJMcaajwQF6/SdCXO4HSnRrGiPqUep0ik1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fll3q/dJMcaajwQF6/SdCXO4HSnRrGiPqUep0ik1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fll3q/dJMcaajwQF6/SdCXO4HSnRrGiPqUep0ik1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFll3q%2FdJMcaajwQF6%2FSdCXO4HSnRrGiPqUep0ik1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1048&quot; height=&quot;592&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2번 과정을 보면, 결재문서이력 테이블 (EAI를 통해 전달받은 데이터가 있는 인터페이스 테이블)을 처리해서 업무 테이들에 결재문서 데이터들을 연동하고, 감사팀에게 발송할 메시지를 별도의 outbox테이블에 적재하는데요. 이 과정들을 하나의 DB 트랜잭션에서 처리합니다. 그리고 outbox테이블에 있는 메시지들을 조회해서 알림을 발송하는 별도의 작업이 10분 주기로 수행되도록 만들었습니다. 이렇게 하면 outbox테이블을 조회해서 알림을 발송하는 작업이 10분마다 수행되므로, 알림 발송이 실패해도 성공할 때까지 재시도가 자연스럽게 진행되게 됩니다. 따라서 최소 1번은 알림을 보낼 수 있게 되는 것이고, 이를 통해 결재문서 데이터 연동과 알림 발송이라는 두 작업이 결과적으로 모두 진행되게 하여 원자성을 보장할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;정말 트랜잭션 아웃박스 패턴이 필요했을까?&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;'데이터 연동 &amp;amp; 알림 발송'이라는 기능을 구현하는 과정이 트랜잭션 아웃박스 패턴을 곁들이면서 다소 복잡해지긴 했습니다만, 데이터 연동과 알림 발송이라는 두 작업의 원자성을 보장해야 하는 것이 우선이었던 만큼 이런 복잡함은 감수해야 했다는 생각이었습니다. 그리고 감사팀 입장에서 왜 이 기능이 필요할지를 직접 고민해보면서 두 작업의 원자성이 필요했던 것을 스스로 도출했던 것이었고, 이를 보장하는 방법들을 여러 가지로 알아보고 스스로 가장 괜찮은 방법이라고 생각되는 것을 적용했던 만큼 솔직하게는 나름대로의 뿌듯함도 있었습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 시간이 흐르고 다시 이 기능 개발을 회고해봤습니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;정말 트랜잭션 아웃박스 패턴이 필요했던 것일까?&lt;/b&gt;&lt;/span&gt; 다시 시간을 되돌린다고 해도 데이터 연동과 알림 발송이라는 두 작업의 원자성은 필요했던 상황이라고 생각합니다. 하지만 정말 트랜잭션 아웃박스 패턴이 당시의 최선이었을까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;뭐 다시 회고해보니, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;굳이 필요하지 않았었다..&lt;/b&gt;&lt;/span&gt; 라는 생각이 들었습니다. ㅎㅎ..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&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;2060&quot; data-origin-height=&quot;976&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cs3Zq8/dJMcabbFNgw/U3xJJDYjfHK4BcNSBl2RQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cs3Zq8/dJMcabbFNgw/U3xJJDYjfHK4BcNSBl2RQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cs3Zq8/dJMcabbFNgw/U3xJJDYjfHK4BcNSBl2RQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcs3Zq8%2FdJMcabbFNgw%2FU3xJJDYjfHK4BcNSBl2RQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2060&quot; height=&quot;976&quot; data-origin-width=&quot;2060&quot; data-origin-height=&quot;976&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 방법을 당시 적용하지 않았던 것은 앞서 설명드렸듯이 커밋 과정에서 예외가 터지면 &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;알림은 발송됐지만 트랜잭션은 롤백되어 데이터가 연동되지 않기 때문&lt;/b&gt;&lt;/span&gt;이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 다시 생각해보면, 어차피 데이터 연동 &amp;amp; 알림 발송은 10분 주기로 반복되고 있습니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;즉 &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;이렇게 알림이 발송됐지만 데이터가 연동되지 않은 케이스가 발생됐다고 해도,&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt; 10분 뒤 혹은 그 뒤에 이 프로세스가 재수행되면서 결국 데이터가 연동&lt;/b&gt;&lt;/span&gt;됩니다. 결국은 이 방법을 써도 결과적으로는 데이터 연동과 알림 발송의 원자성이 보장되는 것입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;트랜잭션 아웃박스 패턴은 결국 쓸 필요가 없던 거였습니다 ㅎㅎ.. 데이터 연동과 알림 발송이라는 두 작업의 원자성을 보장해야 한다는 제약을 스스로 도출해냈었고, 이를 위한 여러 방법을 스스로 고민하면서 오버 엔지니어링 없이 가장 적절한 선택을 했던 거라고 생각했는데, 사실 이조차도 오버 엔지니어링이었겠다는 생각에 당혹스럽기도 하고 멘탈이 조금.. 힘들기도 했습니다. 리팩토링은 바로 진행했었지만,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;아 내가&amp;nbsp; 더 넓은 관점에서 보지 못했던 건가? 좁은 관점에서 기술 적용에만 순간적으로 몰두했던 거였나? 불필요한 과한 설계를 내가 적용하고 있던 걸까?&quot;&lt;/b&gt;&lt;/span&gt; 라는 생각에 스스로를 조금 갉아먹기도 했던 것 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;그래도.. 앞으로가 중요한 것 아니겠습니까!라고 생각을 고쳤습니다. 데이터 정합성 보장이라는 좁은 관점에 매몰되어 10분 주기로 반복 수행되고 있다는 더 큰 관점에서의 맥락을 놓쳤었고, 결국 간단한 길을 두고 다소 돌아간 셈이 됐지만, 이를 통해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;어떻게 구현할까 보다 왜 필요한가를 좀 더 신중하게 다시 살펴보자&lt;/b&gt;&lt;/span&gt;는 교훈을 얻을 수 있었습니다. 완벽함을 위한 노력이 사실은 복잡함을 더할 수 있는 욕심이 될 수 있음을 배웠고, 덕분에 단순함이 주는 강력함도 느낄 수 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;또한 이 경험을 성장을 위한 발판으로 삼고, 제가 했던 고민의 흔적들로 남길려고 합니다. 단순히 데이터 정합성을 고려하지 않고 개발했다면 트랜잭션 아웃박스 패턴이라는 일종의 오버 엔지니어링은 없었겠지만, 반면 데이터 정합성을 지켜주는 방법과 기술들에 대해 이렇게 고민하지 않았을 거라고 생각합니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;이 경험에서 학습한 것들을 발판으로 삼아, 앞으로 기술적인 의사결정들을 내릴 때마다 조금 더 넓은 관점에서, 조금 더 신중하게 고민하게 하는 양분&lt;/b&gt;&lt;/span&gt;으로 삼으려고 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>생각정리, 일상</category>
      <category>transactional outbox pattern</category>
      <category>개발자</category>
      <category>백엔드</category>
      <category>트랜잭셔널 아웃박스 패턴</category>
      <category>트랜잭션</category>
      <category>트랜잭션 아웃박스 패턴</category>
      <category>회고</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/152</guid>
      <comments>https://jofestudio.tistory.com/152#entry152comment</comments>
      <pubDate>Sun, 14 Dec 2025 18:43:02 +0900</pubDate>
    </item>
    <item>
      <title>이상적인 개발자에 대한 고민</title>
      <link>https://jofestudio.tistory.com/151</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;올해 6월?부터 시작한 사이드 프로젝트 동아리 활동이 마무리가 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개인 일정 상 마지막 발표회(이 동아리에서는 데모데이라 불렀음)에 참여하지 못한 건 아쉽게 됐으나, 어찌됐든 마무리되어 시원섭섭한 느낌. 끝나고 돌아보니 이런저런 생각이 많이 든다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아무래도 왜 사이드 프로젝트 동아리에 지원했는가? 그 목적은 뭐였는가? 부터 생각하게 된다. 이유는 심플하게 &quot;성장하고 싶어서&quot;였다. 현재 주어진 회사 업무는 여러 직군과 협업하는 것이 아니었고 내가 할 수 있는 영역에 한계가 있었기 때문에, 회사 밖에서 내가 갈증을 느끼고 있는 영역에 대한 경험을 쌓고 싶었다. 동아리 활동이 시작된 직후, 회사에서 디자이너와 PM으로 일하고 있는 동기들을 찾아가 &quot;함께 일하고 싶은 개발자&quot;가 어떤 사람인지 물어가며 이번 동아리 활동을 그렇게 해야겠다고 다짐했던 일들도 생각난다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;동아리 활동을 하며 내가 몰랐던 영역도 꽤 만나서 즐거웠다. FE파트에서 코드래빗이란 툴을 사용해 PR 리뷰를 자동화하는 걸 보며 &quot;와 이거 회사에서도 쓸 수 있음 좋겠다&quot;했던 기억이 있다. (&lt;i&gt;&lt;s&gt;분명 내가 예전에 대외활동할 땐 소나큐브로 정적분석하던게 전부였는데..&lt;/s&gt;&lt;/i&gt;) BE파트에서 같이 하던 동료의 코드를 보며 &quot;이렇게도 할 수 있군&quot; 했던 기억도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아쉬웠던 것들도 당연히 떠오른다. &quot;좀 더 적극적으로 개입하고 제시할걸&quot;이란 생각이 떠오른다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;동아리 활동을 하며 막바지 즈음에, 같이 하던 디자이너분이 내게 &lt;i&gt;&quot;XX님은 기획 단계에서 막 적극적으로 하진 않으시더라구요? 다른 활동에서는 백엔드 개발자 분들이 기획 단계에서도 엄청 반발하고 하던데..&quot; &lt;/i&gt;라는 멘트를 했다. 단순히만 보면, 내가 했던 서비스는 매우 심플해서 기획이 변경됐다고 기존의 설계가 뒤엎어지는 경우는 없었기 때문에 백엔드 개발자 입장에서 반발해야 하는 상황은 없던 편이었다. 하지만 스스로 많은 생각이 들게 한 멘트는 &quot;&lt;i&gt;적극적으로 하지는 않더라&lt;/i&gt;&quot; 라는 거였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;사실 나는 이 동아리를 하면서 가지고 있던 제일 큰 마인드는 &amp;lt;일단 완성시키기&amp;gt;였다. 소프트웨어 마에XXX 시절, 팀원들의 찐텐 싸움으로 팀이 괴멸하게 되어 실제로 눈물을 훔쳤던 기억이 내 내면에 너무 강하게 남아있었기 때문에.. &quot;일단 완성시키면 뭐라도 된다!&quot;라는 생각을 가지고 있었다. 그래서 기획/디자인 단계에서 &quot;~도 필요하다, ~대한 정책을 정의해야 한다&quot;라는 코멘트를 하는 것 외에는 그렇게까지 깊게 관여하지 않았었고, 특정 기능을 반드시 도입하자고 강하게 주장하지도 않았었다. 빠르게 방향성을 잡고 완성시켜야 한다는 생각으로 어떻게보면 기획이라는 영역에서는 다소 수동적인 자세로 임했던 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;내가 취했던 관점이 잘못됐다라곤 생각하진 않는다. 시간이 촉박했던만큼 빠르게 방향성을 정해서 개발해야하는 상황이었다고 생각한다. 다만 사실 나도 내가 개발하고 싶던 기능(실시간 통신)이 있던 만큼 기획에서도 적극적으로 내가 하고 싶던 걸 어필했으면 어땠을까 싶다. 단순히 &quot;개발자로서 기술적 성취를 얻고 싶은 마음&quot;만 가지고 특정 기능을 반드시 넣어야 한다고 밀어붙이는 건 조금 지양해야겠지만, 사실 생각해보면 실시간 통신으로 우리 서비스에 가져올 수 있던 분명한 메리트가 있었고 이걸 토대로 설득을 해봤으면 어땠을까라는 생각이다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;물론 그때 내가 설득을 했어도 어떻게 됐을지 모르지만, 그렇게 &quot;설득을 해보는 경험&quot;을 이 동아리에서 별로 못 해본(어떻게 보면 내가 안 한 거지만) 것이 가장 큰 아쉬움이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;백엔드 개발자로서 성장하는 방법에 대해 여러 가지를 시도해왔고 지금도 고민하고 있다. &quot;잘 하는 개발자&quot;가 어떤 사람인가에 대해서도 항상은 아니지만 알게 모르게 계속 고민하게 되는 것 같다. 여러 기술을 잘 알고 있는 개발자. 한 영역을 깊게 파고들어서 그 영역을 누구보다 잘 아는 개발자. 문제가 발생했을 때 어디를 봐야하는지 딱 딱 내놓을 수 있는 개발자 등등.. 이렇게 하드스킬 위주로 연마를 해야한다고 생각해왔지만, 요즘 드는 생각은 소프트 스킬이 중요하다는 것이다. 결국 엔지니어는 본인이 연마하고 익힌 기술들을 토대로 문제를 해결하는 사람이므로, 기술을 연마하는 것도 중요하겠지만 결국 &quot;풀어야 할 문제를 정확히 식별&quot;할 수 있어야 한다는 생각이다. 그렇기 위해선 커뮤니케이션을 잘 해야 하고, 팀원들을 잘 설득할 수 있어야 한다. &quot;문제가 이거니까 이런이런 기술을 사용하면 ~하게 해결할 수 있어요!&quot;라고 말할 수 있어야 한다. 물론 이렇게 말해도 그 상황에서는 오답일 수 있을 것이다. 하지만 배가 움직이려면 그 순간엔 잘못된 방향이어도 누군가는 결국 키를 잡아야 하듯이, 이렇게 내 생각을 밖으로 꺼내며 소통해야만 더 나은 해결책으로 귀결될 수 있다. 그렇기 때문에 참 뻔한 말이지만 잘 하는 개발자는 소프트 스킬이 좋은 개발자라고 주변에서 말하는게 아닐까라는 생각이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;하지만 나는 내 생각을 밖으로 꺼내서 소통하는 것에 막 익숙하진 않은 것 같다. 회사에서도 내 생각을 밖으로 꺼내서 팀원들을 설득해야 하는 상황이 실제로 곧 생긴다. 앞으로 개발자로 살면서, 어느 회사가 됐든 소속 팀의 팀원들과 소통하면서 설득하는 일이 정말 많을 것이다. 비개발자 팀원들에게도 설득하는 일이 생길 수 있고, 당장 팀원들부터 설득하지 못한다면 내가 일하고 싶어하는 서비스회사들의 고객들을 설득하는 것은 더 힘들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그렇기 때문에, 이번 사이드 프로젝트에서 그런 &quot;설득하는 경험&quot;을 별로 못 해본 것 같아 참 아쉽다. 기술적인 의사결정이 아니더라도 어떤 형태로든 내 의견과 생각을 피력하고 설득할 수 있었던, 그런 지점들이 생각난다. 앞으로는 적극적으로 내 생각과 의견을 피력하는 태도를 가져야 할 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 활동한 동아리는 젝트라는 동아리로, 관심있으신 분들은 지원해보셔도 될 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://ject.kr&quot;&gt;https://ject.kr&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;608&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BmjG5/dJMb9PzCZMT/Xicc01amMsXkTZ8rjzGKa0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BmjG5/dJMb9PzCZMT/Xicc01amMsXkTZ8rjzGKa0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BmjG5/dJMb9PzCZMT/Xicc01amMsXkTZ8rjzGKa0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBmjG5%2FdJMb9PzCZMT%2FXicc01amMsXkTZ8rjzGKa0%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;378&quot; height=&quot;249&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>생각정리, 일상</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/151</guid>
      <comments>https://jofestudio.tistory.com/151#entry151comment</comments>
      <pubDate>Wed, 22 Oct 2025 00:28:10 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 트리거(Trigger) 살펴보고 고민해보기</title>
      <link>https://jofestudio.tistory.com/150</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 트리거란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;RDB에서 특정 테이블에 이벤트(CREATE or UPDATE or DELETE)가 발생했을 때 수행할 작업들을 정의할 수 있는데, 이를 트리거라고 부릅니다. 즉 DB 테이블에 어떠한 변경이 일어날 때 이를 방아쇠(Trigger)삼아 미리 정의된 SQL 코드가 자동으로 실행되는 것으로 이해할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;트리거는 실행 시점을 기준으로 크게 다음과 같이 2종류로 구분할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) BEFORE 트리거&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;UPDATE, INSERT, DELETE 실행 전에 실행되는 트리거입니다. 애플리케이션으로부터 온 잘못된 요청을 거부하는 방식으로 활용 가능하며, UPDATE와 INSERT의 경우 실제 값이 수정/삭제되기 전 그 값을 가공할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) AFTER 트리거&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;UPDATE, INSERT, DELETE 실행 후에 실행되는 트리거입니다. 주로 변경된 데이터를 기반으로 다른 테이블을 업데이트하는 후속 조치를 남기는 식으로 많이 활용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;참고) BEFORE 트리거든 AFTER 트리거든, 트리거에 정의된 작업들은 트리거를 실행시킨 작업과 같은 트랜잭션에서 수행됩니다. 즉 트리거가 실패하면 트리거를 실행시킨 작업도 실패합니다. (&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html&lt;/a&gt;)&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;만약 트리거를 적용 범위 수준으로 구분하면 다음과 같이 2종류로 구분 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) Row-level 트리거&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이벤트가 발생된 행별로 수행되는 트리거입니다. 예를 들어 UPDATE절에 의해 10개의 행이 바뀌면, 이 트리거는 10번 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) Statement-level 트리거&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;변경된 행 수에 관련없이 조작되는 전체 문장(statement)단위로 실행되는 트리거입니다. 전체 배치 작업 전/후 하나의 로직만 실행하면 되는 경우 등에 사용할 수 있습니다. (MySQL의 경우 지원하지 않습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고) SQL Server나 Oracle의 경우 DDL에도 트리거를 만들 수 있습니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 트리거 생성 방법 (MySQL 기준)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;트리거 생성은 CREATE TRIGGER를 통해 수행할 수 있고, 해당 테이블에 대한 Trigger 권한이 필요합니다. 세부적인 생성 문법은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760354739653&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 대괄호는 Optional을 의미합니다.

CREATE
    [DEFINER = user]
    TRIGGER [IF NOT EXISTS] trigger_name
    trigger_time trigger_event
    ON tbl_name FOR EACH ROW
    [trigger_order]
    trigger_body

-- 각 필드
-- 1. trigger_time: { BEFORE | AFTER }
-- 2. trigger_event: { INSERT | UPDATE | DELETE }
-- 3. trigger_order: { FOLLOWS | PRECEDES } other_trigger_name&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;DEFINER : &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;트리거 정의자를 어떤 계정으로 할 것인지 지정&lt;/b&gt;&lt;/span&gt;합니다. 트리거는 실행되면 트리거를 실행시킨 계정의 권한이 아니라 트리거를 정의한 사람의 권한으로 실행되는데, 그때의 트리거 정의자 계정을 지정하는 것으로 이해할 수 있습니다. 이 필드를 따로 작성하지 않으면 트리거를 생성한 사람이 자동으로 DEFINER가 됩니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DEFINER로 지정되는 계정에는 해당 테이블(트리거가 붙는 테이블)에 대한 TRIGGER 권한이 필요&lt;/b&gt;&lt;/span&gt;하며, trigger_body에서 참조되는 테이블들에는 TRIGGER 권한은 필요없으나 적절한 DML 권한이 필요합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;trigger_name : 트리거의 이름으로, 같은 스키마 내에서 트리거 이름은 유일해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;trigger_time : 트리거의 실행시점을 지정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;trigger_event : 트리거를 발생시킬 이벤트 종류를 지정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;trigger_order : 같은 테이블의 동일한 이벤트에 대한 트리거가 여러 개일때, 각각의 실행 순서를 지정합니다. FOLLOWS 사용시 지정한 트리거 실행 후에 실행되고, PRECEDES 사용시 지정한 트리거 실행 전에 실행됩니다. 참고로 MySQL 5.7버전부터만 동일 이벤트에 대한 복수의 트리거 정의가 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;trigger_body : 실행할 작업을 정의합니다. BEGIN ... END를 작성하면 여러 작업을 정의할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;trigger_body에서는 OLD와 NEW라는 키워드를 통해 변경 전후의 데이터에 접근할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;OLD : UPDATE 또는 DELETE 문에서 변경/삭제 전의 기존 데이터를 가리킵니다. OLD.column_name을 통해 해당 데이터의 컬럼값에 접근할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;NEW: INSERT 또는 UPDATE 문에서 새로 추가되거나 변경될 행의 데이터를 가리킵니다. NEW.column_name을 통해 해당 데이터의 컬럼값에 접근할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;UPDATE 문에서는 OLD &amp;amp; NEW를 둘 다 접근할 수 있는데, 당연히 BEFORE 트리거에서는 NEW만 수정 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 내용들을 토대로 트리거 생성 예제를 보면 다음과 같습니다. employees 테이블에 있는 직원 연봉(salary)가 변경될 때마다 변경내용을 salary_audit 테이블에 기록하는 AFTER 트리거입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760361182932&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 트리거 생성도 세미콜론으로 끝나는데, trigger_body 내에서 사용되는 세미콜론이 트리거 종료로 인식되는걸 방지
DELIMITER $$

CREATE DEFINER = 'admin_user'@'localhost'
TRIGGER after_salary_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    -- 연봉이 변경되었을 때만 로그를 남김
    IF OLD.salary &amp;lt;&amp;gt; NEW.salary THEN
        INSERT INTO salary_audit (emp_no, old_salary, new_salary, changed_by)
        VALUES (OLD.emp_no, OLD.salary, NEW.salary, USER());
    END IF;
END$$

DELIMITER ;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 질문. USER()를 통해 변경을 실행한 DB 계정을 기록하는 건 알겠는데, 트리거를 실행시킨 DB 계정이 기록될까요 아니면 DEFINER로 정의된 DB계정이 기록될까요?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제 예제를 통해 보겠습니다. 현재 employees 테이블에 다음과 같은 레코드가 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;1264&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IwDSM/btsQ6HQu7Lq/K652s58ktR4kaeht6TdkpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IwDSM/btsQ6HQu7Lq/K652s58ktR4kaeht6TdkpK/img.png&quot; data-alt=&quot;이쁘게 이미지로 만들었습니다 ㅋㅋ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IwDSM/btsQ6HQu7Lq/K652s58ktR4kaeht6TdkpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIwDSM%2FbtsQ6HQu7Lq%2FK652s58ktR4kaeht6TdkpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;297&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이쁘게 이미지로 만들었습니다 ㅋㅋ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 admin_user, app_user 계정을 만든 뒤 다음처럼 권한을 부여했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;admin_user&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;employees 테이블에 대한 SELECT, TRIGGER 권한&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;salary_audit 테이블에 대한 INSERT 권한&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;app_user&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;employees 테이블에 대한 SELECT, INSERT 권한&lt;/span&gt;&lt;/li&gt;
&lt;/ol&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;1652&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCyPsp/btsQ5SLcypE/cw2DO1KEnfPCAycUNpTxJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCyPsp/btsQ5SLcypE/cw2DO1KEnfPCAycUNpTxJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCyPsp/btsQ5SLcypE/cw2DO1KEnfPCAycUNpTxJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCyPsp%2FbtsQ5SLcypE%2Fcw2DO1KEnfPCAycUNpTxJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;835&quot; height=&quot;557&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;1102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 app_user 계정으로 접속하여 salary를 20000으로 바꿔주었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;salary_audit 테이블에 적재된 결과는..&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;1788&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y26Rw/btsQ8pt4luf/L8QSzQyn811d7k5aREPCWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y26Rw/btsQ8pt4luf/L8QSzQyn811d7k5aREPCWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y26Rw/btsQ8pt4luf/L8QSzQyn811d7k5aREPCWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY26Rw%2FbtsQ8pt4luf%2FL8QSzQyn811d7k5aREPCWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1788&quot; height=&quot;582&quot; data-origin-width=&quot;1788&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음과 같이 트리거 정의자 계정이 아닌, 트리거를 실행시킨 DB 계정이 기록된 것을 볼 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 이 내용은 MySQL 공식 문서 내에서도 확인 가능합니다 (&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. ⭐️ 어디에, 그리고 왜 트리거를 쓸까&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;우선 트리거는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;특정 테이블에 이벤트가 발생됐을 때 수행하고 싶은 기능을 자동화하는 것&lt;/b&gt;&lt;/span&gt;으로 볼 수 있습니다. 애플리케이션에서 반복 작성해야 할 로직을 트리거를 쓰면 한 번만 구현하면 되고, 중요한 데이터에 대한 감사 로깅이 필요한 경우 트리거를 통해 별도의 로그 테이블에 자동으로 기록하게 하거나, 유저 테이블의 회원 상태가 '탈퇴'로 변경됐을 때 관련된 게시판 테이블의 게시물 상태들을 '비공개'로 변경하는 작업 등에 활용 가능합니다. 외부로의 이벤트/메시지 전파를 위해 같은 트랜잭션 내에서 비즈니스 테이블에 데이터를 연동하고 outbox 테이블에 발행할 이벤트를 적재하는 '트랜잭셔널 아웃박스 패턴'의 구현 방법으로 트리거를 채택할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;하지만 단순히 '자동화'를 넘어서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;트리거의 핵심 역할을&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;DB 스스로가 데이터의 정합성과 일관성을 유지하도록 강제하는 것&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;으로 바라볼 수 있습니다. 장애라고 하는 것은 트래픽으로 인해서 뿐만 아니라&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;여러 원인으로 인해 논리적으로 잘못된 형태의 데이터가 적재되면서도 발생할 수 있는데요. 물론 개발자가 애플리케이션 레벨에서 최대한 조심하며 개발할 수 있겠지만 결국 개발자 스스로도 휴먼 에러를 낼 수 있고(예상치 못하게 로직이 꼬인다든가.. 등) 이로 인해 잘못된 데이터가 들어올 수 있습니다. 물론 테이블에 CHECK 등의 제약 조건을 붙이며 최대한 데이터 무결성을 지킬 순 있겠으나, 'VIP 단계의 고객이 주문한 아이템들의 총액은 10,000원을 넘어야 한다' 등의 복잡한 비즈니스 제약들을 표현하고 지키기는 힘듭니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;이럴 때 트리거를 사용해서 복잡한 데이터의 유효성 검사를 담당하게 할 수 있고, 단순 참조 무결성을 넘어서 복잡한 비즈니스 제약을 DB 스스로가 지키도록 강제할 수 있습니다. 또한 트리거 정의시 작성하는 DEFINER 계정에만 A 테이블에 대한 권한을 준다면, 해당 트리거 실행을 제외하고는 A 테이블에 데이터가 넣어질 수 없도록 아예 틀어막아 버릴 수도 있습니다. (개발자 실수 등이 생겨도 잘못된 데이터가 들어가는 일 자체가 안 생기게 됨)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그렇다고 트리거가 늘 좋냐!고 하면 당연히 아닙니다. 다음과 같은 단점들이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;1) 성능 저하&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;애플리케이션에서 실행한 DB 작업 외에도 트리거를 통한 부가 작업이 수행되는 만큼, 성능 저하가 발생됩니다. 특히 MySQL은 Row-level 트리거이므로 1,000개의 행이 갱신되면 트리거가 1,000번 실행되므로.. 만약 트리거가 부착된 테이블의 변경이 잦다면 그만큼 성능 오버헤드도 비례해서 올라갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;2) 불투명성&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&quot;A 테이블에 데이터를 넣으면 B 테이블에도 데이터를 추가한다&quot; 라는 비즈니스 로직이 있다고 가정해보겠습니다. 이 비즈니스 로직이 애플리케이션 레벨에서 우리에게 친숙한 자바 코드 등으로 작성되어 있다면 처음 서비스를 접하는 개발자도 이런 로직이 있다고 쉽게 파악할 수 있습니다. 하지만 트리거를 통해 DB 레벨에서 이 비즈니스 로직이 구현되어 있다면, 개발자가 이런 비즈니스 로직이 있다고 인지하기도 어려울 뿐더러 문제 발생 시 디버깅도 어렵게 됩니다. 더 나아가 한 테이블에 부착된 트리거들이 FOLLOWS 등을 통해 여러 선후행 관계를 가진다면 골치가 더 아파지게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;3) 작동 확인 방법의 번거로움&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;트리거는 작동을 확인하기 위해 명시적으로 실행해볼 수 있는 방법이 없어서, 잘 동작하는지 확인하려면 직접 부착된 테이블에 INSERT나 UPDATE, DELETE를 해줘야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;트리거 사용의 장단점을 표로 정리하면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 강력한 데이터 무결성 보장이 가능&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 관련 작업을 자동화하여 개발 편의성 증가&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 비즈니스 로직의 중앙화 (여러 앱이 같은 DB를 공유시, 모두에게 같은 규칙을 적용)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 성능 저하 유발 가능&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 비즈니스 로직이 드러나지 않게 됨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 디버깅 복잡도 증가&lt;br /&gt;4. 작동 확인을 위해 명시적으로 실행하는 방법이 부재&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그러면 트리거는 어떤 상황에 쓰면 좋고 어떤 상황에는 안 쓰는게 좋을까요? 개인적으로 생각해본 건 다음과 같습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;써도 괜찮을 때&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DB에 직접 접근해서 데이터를 수동으로 수정하는 경우가 많아, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;휴먼 에러를 방지하고 데이터 무결성을 반드시 지켜주고 싶을 때&lt;/b&gt;&lt;/span&gt; (앞서 말한 것처럼 DEFINER로 작성하는 계정에만 특정 테이블에 대한 권한을 주는 식으로 등..)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여러 애플리케이션이 하나의 DB를 공유하고 있어서 모두에게 동일한 규칙을 강제하고 싶을 때&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쓰면 안 좋을 때&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;트리거를 부착하고 싶은 테이블의 데이터 변경이 잦아서 성능 저하가 예상될 때 (MySQL은 특히나 Row-level 트리거이므로)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;비즈니스 로직이 자주 변경될 때 (대부분의 경우 애플리케이션 코드를 수정하는게 더 쉬울 것 같습니다)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;서비스의 로직을 애플리케이션에서만 관리하는게 좋다고 판단될 때&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 알아두면 좋은 트리거 주의사항&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;INSERT INTO ... ON DUPLICATE SET&quot; 는 중복이 없다면 BEFORE INSERT와 AFTER INSERT 이벤트를 발생시키고, 중복이 있다면 BEFORE UPDATE와 AFTER UPDATE 이벤트를 발생시킵니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테이블에 외래키 관계에 의해 자동으로 변경된 경우는 트리거가 호출되지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;트리거 작성 시 BEGIN .. END 블록 내에서는 ROLLBACK / COMMIT 등 트랜잭션을 명시적으로 시작하거나 종료하는 SQL 문장을 작성할 수 없습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;mysql과 information_schema, performance_schema 데이터베이스에 존재하는 테이블들에는 트리거를 생성할 수 없습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. 레퍼런스&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/trigger-syntax.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.4/en/trigger-syntax.html&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/create-trigger.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.4/en/create-trigger.html&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/dml-triggers.html#GUID-E76C8044-6942-4573-B7DB-3502FB96CF6F&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/dml-triggers.html#GUID-E76C8044-6942-4573-B7DB-3502FB96CF6F&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.ibm.com/docs/en/ias?topic=triggers-types&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ibm.com/docs/en/ias?topic=triggers-types&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MySQL</category>
      <category>DB</category>
      <category>MySQL</category>
      <category>trigger</category>
      <category>데이터베이스</category>
      <category>트리거</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/150</guid>
      <comments>https://jofestudio.tistory.com/150#entry150comment</comments>
      <pubDate>Tue, 14 Oct 2025 22:20:12 +0900</pubDate>
    </item>
    <item>
      <title>간만의 사이드 프로젝트 중간 회고</title>
      <link>https://jofestudio.tistory.com/149</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light'; letter-spacing: 0px;&quot;&gt;올해 사이드 프로젝트를 통한 역량 향상의 필요성을 느끼고, 간만에 사이드 프로젝트 동아리를 운좋게 합격해서 들어갔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light'; letter-spacing: 0px;&quot;&gt;현재 MVP 배포를 끝낸 상태에서, 5월 중순에 진행한 온보딩 기간부터 현 시점까지의 회고를 짧게나마 작성해보려 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div id=&quot;SE-85832396-58a9-4293-acb0-0a1ccd439541&quot; data-a11y-title=&quot;본문&quot; data-compid=&quot;SE-85832396-58a9-4293-acb0-0a1ccd439541&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-85832396-58a9-4293-acb0-0a1ccd439541&quot; data-unitid=&quot;&quot;&gt;
&lt;div id=&quot;SE-87d0390e-2a1c-43a3-9663-1ad84a8b8640&quot;&gt;
&lt;p id=&quot;SE-dcdec015-ff2f-498b-bc3e-a2628c514e85&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-93ac74a4-ce66-476a-87c5-988069f8e9f5&quot; data-a11y-title=&quot;소제목&quot; data-compid=&quot;SE-93ac74a4-ce66-476a-87c5-988069f8e9f5&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-93ac74a4-ce66-476a-87c5-988069f8e9f5&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-38d52509-df79-436f-829e-a2d3fd4f3a9a&quot;&gt;
&lt;h3 id=&quot;SE-bb306f08-cea2-413b-95fd-4700838c5934&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;온보딩 ~ MVP 배포 기간 동안 마주한 상황들에 대한 해결 과정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;제가 올해 사이드 프로젝트를 해야겠다고 생각한 것은, 기술 역량 향상도 목적이 있긴 했으나 협업 상황에서 발생하는/발생 가능한 여러 상황들에 대해 능동적으로 다양한 방법들을 써보며 &quot;아 이런 상황에선 이런 방법을 쓰면 더 효율적으로 일을 굴릴 수 있구나&quot; 같은 것들을 익히고 싶던 것도 있습니다. 그래서 이번 프로젝트에서 나름대로 여러 가지를 먼저 제안하고 공유했는데, 그런 것들 위주로 회고글을 작성해봤습니다. (그래서 기술적인 관점에서 고민했던 것들은 최대한 이 글에서는 배제하고, 일을 굴리는 관점에서 고민했던 곳들 위주로 작성했습니다)&lt;/span&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;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 기능명세서와 와이어 프레임 등이 미흡했던 문제&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div id=&quot;SE-7a5d7a37-8955-4a18-b262-de0d90384b2a&quot; data-a11y-title=&quot;본문&quot; data-compid=&quot;SE-7a5d7a37-8955-4a18-b262-de0d90384b2a&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-7a5d7a37-8955-4a18-b262-de0d90384b2a&quot; data-unitid=&quot;&quot;&gt;
&lt;div id=&quot;SE-554121bb-e804-4a61-b592-5256e0e8f639&quot;&gt;
&lt;p id=&quot;SE-004beab7-99d0-4fb0-ae3b-89700a9e8aa7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;기획팀으로부터 작성된 기능명세서를 받았으나 개발 관점에서 보면 정의가 좀 더 필요하다고 생각되는 부분들이 보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-b753cc6e-faa0-4198-a1fa-66f3979b627b&quot; data-a11y-title=&quot;사진&quot; data-compid=&quot;SE-b753cc6e-faa0-4198-a1fa-66f3979b627b&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-b753cc6e-faa0-4198-a1fa-66f3979b627b&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-b753cc6e-faa0-4198-a1fa-66f3979b627b&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;&quot; data-unitid=&quot;SE-b753cc6e-faa0-4198-a1fa-66f3979b627b&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QW7vi/btsQcnE5I3j/dPkwTsc3Lzkk9bVm2hgfUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QW7vi/btsQcnE5I3j/dPkwTsc3Lzkk9bVm2hgfUK/img.png&quot; data-alt=&quot;처음에 받은 기능명세서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QW7vi/btsQcnE5I3j/dPkwTsc3Lzkk9bVm2hgfUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQW7vi%2FbtsQcnE5I3j%2FdPkwTsc3Lzkk9bVm2hgfUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;709&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;709&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;처음에 받은 기능명세서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #555555; font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-59769983-3df7-4716-a56d-c71e303e04a2&quot; data-a11y-title=&quot;본문&quot; data-compid=&quot;SE-59769983-3df7-4716-a56d-c71e303e04a2&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-59769983-3df7-4716-a56d-c71e303e04a2&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-5d47401f-cd95-4104-bad5-4966f7a8ece9&quot;&gt;
&lt;p id=&quot;SE-3857576b-634a-4a90-b03c-2a9098bd15dc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-37e95be7-b188-40f8-b271-62d73ae0a52c&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;구체적으로 이 기능이 동작할 때 어떤 데이터들을 사용하고 어떤 데이터들을 받는지, 이 기능이 어떤 흐름과 조건에서 동작하는지, 어떤 유효성과 예외가 있는지에 대한 정의가 다소 부족하다고 느꼈습니다. 또한 세부적인 정책 정의가 필요하다고 느끼는 부분들도 있었습니다(~를 하면 ~를 할 수 없게 한다 등).&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-89ba8dd1-eebd-422f-a4a6-f00b22e1a646&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;암튼 이런 부분들을 기획팀에게 되물어봐서 정확한 스펙 정의를 해야 했는데, 이걸 4명의 개발자 각자가 기획팀에게 물어보면 불필요한 핑퐁이 더 많아지고 오히려 정리하기 더 힘들어질 것 같다고 생각했습니다. 그래서 노션 페이지를 파서 개발자끼리 기능 명세서에 추가적으로 정리가 필요하다고 생각되는 부분들을 취합하고 이 문서를 기획팀으로 전달하자고 제안을 드렸는데요, 다들 좋다고 해주셔서 이 방법을 적용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-ce56d871-5ea9-4882-96df-48938c9fce51&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-8232beea-d3d4-469f-b583-5eab71722618&quot; data-a11y-title=&quot;사진&quot; data-compid=&quot;SE-8232beea-d3d4-469f-b583-5eab71722618&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-8232beea-d3d4-469f-b583-5eab71722618&quot; data-unitid=&quot;&quot;&gt;
&lt;div id=&quot;SE-8232beea-d3d4-469f-b583-5eab71722618&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;&quot; data-unitid=&quot;SE-8232beea-d3d4-469f-b583-5eab71722618&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csjr2h/btsQc21MEY1/TjKY6mQCrp1K5d885C7zD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csjr2h/btsQc21MEY1/TjKY6mQCrp1K5d885C7zD0/img.png&quot; data-alt=&quot;추가 정의가 필요한 사항들을 취합했던 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csjr2h/btsQc21MEY1/TjKY6mQCrp1K5d885C7zD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcsjr2h%2FbtsQc21MEY1%2FTjKY6mQCrp1K5d885C7zD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;699&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;699&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;추가 정의가 필요한 사항들을 취합했던 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-83362ae4-4894-4633-b93c-90edd62d7213&quot; data-a11y-title=&quot;본문&quot; data-compid=&quot;SE-83362ae4-4894-4633-b93c-90edd62d7213&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-83362ae4-4894-4633-b93c-90edd62d7213&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-b65ecf93-4347-4701-b582-069b8d8193a4&quot;&gt;
&lt;p id=&quot;SE-4dff4571-ec72-4436-8e90-58fc33080378&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-aaf5eb8c-fcdc-4457-83c2-9e9ceb5c2c67&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 프로젝트 진행 과정 중 전반적으로 기획팀과 개발자 사이에 &quot;이거는 뭘로 해야 해요?&quot;라던지 &quot;이거는 어떤 값 써야 해요?&quot;를 주고받으며 핑퐁하는데 시간도 많이 걸리고 서로 스트레스도 많이 받는 것 같다고 느꼈습니다. 사실 백엔드는 화면에 대한 영향도를 크게 받지는 않지만 프론트엔드는 화면에 대한 영향도도 크게 받기 때문에, 특히 프론트엔드 개발자와 기획팀 간에 이런 일이 많은 듯 합니다. &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;뭐 암튼.. 그럴 때는 차라리 개발자들이 먼저 기획팀에게 본인들이 원하는 형태로 기능명세서나 화면 설계서 등을 작성해달라고 요청해주는 게 좋겠다는 생각&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 들었습니다. 아무래도 PM이나 PD 직군은 개발을 직접 해보지 않은 이상 개발 친화적(?)인 문서 작성을 처음부터 하기에는 힘들 것 같기 때문에 아싸리 처음부터 개발자들이 원하는 형태의 기능명세서 템플릿을 드리면 어떨까라는 생각입니다. 저는 개인적으로 기능명세서는 다음과 같은 템플릿으로 작성되면 좋겠다는 생각이 이번에 든 것 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-33c61334-7c6b-41e4-8332-398b76d59c04&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-a1123efa-0b36-4bd0-92af-02396c3f2f00&quot; data-a11y-title=&quot;사진&quot; data-compid=&quot;SE-a1123efa-0b36-4bd0-92af-02396c3f2f00&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-a1123efa-0b36-4bd0-92af-02396c3f2f00&quot; data-unitid=&quot;&quot;&gt;
&lt;div id=&quot;SE-a1123efa-0b36-4bd0-92af-02396c3f2f00&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;&quot; data-unitid=&quot;SE-a1123efa-0b36-4bd0-92af-02396c3f2f00&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;833&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/umNQ1/btsQfkUsEeb/lC9fKZ7GNpsjWbwI5KxoJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/umNQ1/btsQfkUsEeb/lC9fKZ7GNpsjWbwI5KxoJ0/img.png&quot; data-alt=&quot;내가 생각하는 개발 친화적(?)인 기능 명세서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/umNQ1/btsQfkUsEeb/lC9fKZ7GNpsjWbwI5KxoJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FumNQ1%2FbtsQfkUsEeb%2FlC9fKZ7GNpsjWbwI5KxoJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;833&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;833&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내가 생각하는 개발 친화적(?)인 기능 명세서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;SE-2cffb660-52e6-44bf-8640-2c78fa068e9c&quot;&gt;
&lt;p id=&quot;SE-3d4d7670-6f49-40f0-a80f-047976fb4529&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-c0674902-7347-4a1d-8669-c2c0fea2258d&quot; data-a11y-title=&quot;본문&quot; data-compid=&quot;SE-c0674902-7347-4a1d-8669-c2c0fea2258d&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-c0674902-7347-4a1d-8669-c2c0fea2258d&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-67e82966-e408-4dd9-85a0-2d4dc024260b&quot;&gt;
&lt;p id=&quot;SE-ddad35a7-1c17-44bb-8493-f8c293924cec&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;물론 하나의 예시긴 하나, 위 형태로 기능명세서가 작성된다면 데이터의 흐름(입력과 출력)이 잘 드러나고 예외와 조건도 잘 명시되어 있다고 생각합니다. 그래서 다음에 또다시 사이드 프로젝트를 하게 된다면 아싸리 처음부터 이런 양식으로 만들어달라고 해보는 것도 괜찮다는 생각입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-e711d1c8-5f8a-4e14-af8b-70fc6a034484&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-bac8892f-f65d-4da9-bb9c-e039f27e82cf&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;SE-c05614a6-9987-491d-9816-684e87e467b7&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 기획/디자인 기간이 늘어나던 문제&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p id=&quot;SE-18d14b9d-a656-4185-87ba-97d61a1367b3&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대부분 기획단계의 산출물이 나와있어야 개발이 가능하기 때문에 기획과 디자인이 기간이 늘어나면 개발자는 붕 뜨게 될 수도 있는데요, 개인적으로 프로젝트 극 초기부터 이런 상황이 발생할 수 있다고 생각했습니다. 그래서 제가 적용했던 방법은 &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;처음부터 개발자 분들끼리 서로 먼저 할 수 있는 것들을 해두자고 말한 것&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;. 동일직군 개발자끼리 서로 사용할 기술스택과 버전을 맞춰 둔다든가, 커밋 컨벤션 등을 정해둔다든가, 프로젝트 공통 설정 세팅을 미리 한다든가 등등.. 또한 개인적으로 로그인/회원가입 기능은 MVP 단계에서 필요없다고 생각하지만 기획과 디자인 기간이 생각보다 더 늘어난다면 미리 만드는 게 좋겠다 싶어 이것도 미리 해보자고 할려고 했으나, MVP 단계에서 로그인 기능을 넣는 것이 다소 늦게 결정되어 로그인 기능을 미리 만들지는 못 했습니다 (프로젝트 설정을 좀 더 빨리 했으면 미리 만들 수 있었겠다라는 아쉬움이 있네요)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-ebf53561-8c4c-414a-abd8-7ac11ebd124e&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-c3976fe8-3399-4b8c-b9c2-bffde6f9fad6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;SE-d0a97762-05db-4c29-be26-191c371e9e82&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 프로젝트 공통 설정에 대한 문제&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p id=&quot;SE-8ae3c3ad-c264-4066-ace0-e1811f8d30e1&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;백엔드 파트는 이번에 멀티 모듈을 도입하여 프로젝트를 해보기로 했고, 이에 대한 프로젝트 세팅을 제가 하게 됐습니다. 하지만 함께 협업할 백엔드 개발자 분이 Spring을 주력으로 하시던 분이 아니기도 했고, 멀티 모듈 프로젝트가 생소한 개념일 수 있어 많이 헷갈려 하실 수 있겠다는 생각이 들었습니다. 그래서 프로젝트 세팅에 대한 가이드 문서를 만들면 좋겠다는 생각에 다음과 같이 노션에 프로젝트 가이드 문서를 만들어서 공유드렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-d393f6c9-a18f-41b8-ae06-8ef2822ea304&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-7fe0dba1-151c-4e77-8364-85341023ab44&quot; data-a11y-title=&quot;사진&quot; data-compid=&quot;SE-7fe0dba1-151c-4e77-8364-85341023ab44&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-7fe0dba1-151c-4e77-8364-85341023ab44&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-7fe0dba1-151c-4e77-8364-85341023ab44&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;&quot; data-unitid=&quot;SE-7fe0dba1-151c-4e77-8364-85341023ab44&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIxTUl/btsQfBuQLYW/n7hrgZMkEZLs91giBECsr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIxTUl/btsQfBuQLYW/n7hrgZMkEZLs91giBECsr0/img.png&quot; data-alt=&quot;다이어그램도 열시미 그렸습니다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIxTUl/btsQfBuQLYW/n7hrgZMkEZLs91giBECsr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIxTUl%2FbtsQfBuQLYW%2Fn7hrgZMkEZLs91giBECsr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;704&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다이어그램도 열시미 그렸습니다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-093338a9-3d32-4e35-88a4-969ed9821a7a&quot; data-a11y-title=&quot;본문&quot; data-compid=&quot;SE-093338a9-3d32-4e35-88a4-969ed9821a7a&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-093338a9-3d32-4e35-88a4-969ed9821a7a&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-d37ecbcc-5c38-44cf-8319-1bf7d0fd2a47&quot;&gt;
&lt;p id=&quot;SE-019103e7-3247-4092-acc3-0518152fc66a&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;SE-2e7c9b98-1d6d-4127-84a2-34563380144f&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. api 명세와 배포 전 api 테스트 방안에 대한 문제&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p id=&quot;SE-39e73bad-8a98-4d50-b3c6-7fa65c2cc13e&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프론트엔드 분들이 어떻게 백엔드 파트에서 개발한 api에 대해 연동 테스트를 하도록 할지도 고민이었습니다. 회사라면 스테이징 서버와 개발 서버를 운영하고 있기에 개발 서버에 백엔드 파트에서 개발한 내용을 배포해두면 프론트엔드 분들이 개발 서버로 테스트를 해볼 수 있지만, 사이드 프로젝트에서는 비용 이슈로 그렇게 하기는 힘들었습니다. 그렇다고 프론트엔드 분들이 본인 로컬에 백엔드 서버를 직접 띄워서 테스트하는 건 비효율적이라 생각해서 같이 회의를 했었는데, &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;&lt;b&gt;백엔드 파트에서 OpenApi 스펙의 yaml파일을 만들어서 주면 프론트 파트에서 Mock서버를 만들어서 테스트하는 식으로 합의&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 보게 됐습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-8ddacb9c-c522-4390-962a-0ff70d6ee1cc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-ae6fc097-cc04-4d6b-958d-998af16207d3&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;우선은 개발에 들어가기 전에 노션을 통해 api 명세 문서를 만들었고, ai의 힘을 활용해 노션에 작성했던 명세를 openapi 스펙의 yaml로 변환하여 프론트 파트로 전달했습니다. &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;그리고 api 명세 수정이 있을 때마다 명세 불일치 문제를 만드는 것을 피하기 위해 즉시 노션에 있던 명세 문서를 수정하고 yaml파일을 바꿔서 다시 전달드리는 식으로 운영했는데, 수기로 노션 문서까지 수정해야 하니 당연히 불편&lt;/span&gt;&lt;/b&gt;했습니다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그래서 yaml파일을 커밋하면 레포지토리에 커밋된 yaml파일을 읽어 redoc 문서를 정적으로 배포해주는 워크플로우를 등록해서 다음과 같이 api 명세서 문서를 운영&lt;/b&gt;&lt;/span&gt;하도록 전략을 바꿨습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-2e290da2-ab4b-4ff5-ba3c-51852d44715c&quot; data-a11y-title=&quot;사진&quot; data-compid=&quot;SE-2e290da2-ab4b-4ff5-ba3c-51852d44715c&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-2e290da2-ab4b-4ff5-ba3c-51852d44715c&quot; data-unitid=&quot;&quot;&gt;
&lt;div id=&quot;SE-2e290da2-ab4b-4ff5-ba3c-51852d44715c&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;&quot; data-unitid=&quot;SE-2e290da2-ab4b-4ff5-ba3c-51852d44715c&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJ5gN/btsQdeIiEhU/B3y8RKMaEN1nxEHL09m4i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJ5gN/btsQdeIiEhU/B3y8RKMaEN1nxEHL09m4i0/img.png&quot; data-alt=&quot;api 명세 수정 시 더 이상 노션은 수기로 건드리지 않아도 됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJ5gN/btsQdeIiEhU/B3y8RKMaEN1nxEHL09m4i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJ5gN%2FbtsQdeIiEhU%2FB3y8RKMaEN1nxEHL09m4i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;558&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;api 명세 수정 시 더 이상 노션은 수기로 건드리지 않아도 됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;SE-3ae96cdc-f8ee-4afa-a860-8d515adfbe5e&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-fa5a25c7-0367-4195-b6d9-750efc6131f1&quot; data-a11y-title=&quot;본문&quot; data-compid=&quot;SE-fa5a25c7-0367-4195-b6d9-750efc6131f1&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-fa5a25c7-0367-4195-b6d9-750efc6131f1&quot; data-unitid=&quot;&quot;&gt;
&lt;div id=&quot;SE-00d741b6-b20e-4db7-813b-c962c5d7f737&quot;&gt;
&lt;p id=&quot;SE-ed6d4711-b341-4aa6-b721-f8f718aa657e&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;다만! yaml파일은 아직 수기로 수정하고 있습니다. 백엔드 단 기능 변화가 있을 때마다 그에 따라 yaml파일을 자동으로 재추출하도록 워크플로우를 등록하면 아예 api 명세 문서 배포 플로우를 자동화할 수 있지만 이 부분은 우선순위 이슈로 현 시점에서는 아직 설정을 안 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-bf7c63b4-a420-48ea-8096-15b879d33253&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-9bf81e06-11ad-4152-a28a-36de6d73883b&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;SE-44f2f38c-d8ba-4b65-9f64-f28a28a50b05&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. 기능 흐름에 대한 세부적인 정리가 필요했던 문제&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p id=&quot;SE-b199e597-2ad8-457f-96e4-a2beb587b67d&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;대개의 경우 신규 리소스 등록(Create) 시 해당 리소스의 key값을 클라이언트에서 지정하지 않습니다. 하지만 저희 서비스는 게임을 저장할 때 해당 게임을 구성하는 이미지들의 경로도 함께 저장해야 하며, 이 이미지들은 백엔드에서 준 S3 presigned url에 업로드하게 되는데요. 이때 저는 관리의 용이성을 위해 S3 버켓에 해당 게임 아이디(key값)로 디렉토리를 만들고 그 하위에 다음과 같이 저장하도록 하고 싶었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccVJR3/btsQc0CTTqD/N96XRn3YDOh2gYHXduNwr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccVJR3/btsQc0CTTqD/N96XRn3YDOh2gYHXduNwr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccVJR3/btsQc0CTTqD/N96XRn3YDOh2gYHXduNwr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccVJR3%2FbtsQc0CTTqD%2FN96XRn3YDOh2gYHXduNwr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;230&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;다만 이렇게 할려면 백엔드에서 클라이언트로 presigned url을 발급해줄 때 해당 게임이 사용하는 gameId(key값)를 서버가 알고 있어야 하는데요. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이때 문제는 게임을 신규로 등록하는 경우는 gameId를 어떤 걸 쓰는지 모르기 때문에, 게임을 신규로 등록할 때 게임이 사용하게 될 gameId를 미리 지정한 다음 다음 이를 활용해&amp;nbsp; presigned url을 발급하는 식으로 게임 등록 흐름을 설계&lt;/b&gt;&lt;/span&gt;했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;이렇다보니 게임을 신규로 등록할 때 해당 게임이 사용하는 key값을 클라이언트가 요청 데이터에 함께 보내는 형태가 되기도 했고, 프론트엔드 분들 입장에선 presigned url에 이미지를 저장하는 것이 생소한 흐름일 수 있겠다는 생각이 들었습니다. 그래서 게임을 등록하는 흐름에 대한 명세 느낌의 문서를 만들면 좋겠다는 생각에 다음처럼 문서를 만들어 공유드렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;1950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVKaAQ/btsQfCtJKfj/PhOGwVOeH2hsyhk4L6SGa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVKaAQ/btsQfCtJKfj/PhOGwVOeH2hsyhk4L6SGa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVKaAQ/btsQfCtJKfj/PhOGwVOeH2hsyhk4L6SGa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVKaAQ%2FbtsQfCtJKfj%2FPhOGwVOeH2hsyhk4L6SGa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2542&quot; height=&quot;1950&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;1950&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8u1jp/btsQgeGe79T/HbKBpNT9K7qybkjOTokI01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8u1jp/btsQgeGe79T/HbKBpNT9K7qybkjOTokI01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8u1jp/btsQgeGe79T/HbKBpNT9K7qybkjOTokI01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8u1jp%2FbtsQgeGe79T%2FHbKBpNT9K7qybkjOTokI01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1958&quot; height=&quot;180&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프론트엔드 개발자 분이 다음처럼 메시지 남겨주셨었는데 나름 뿌듯한 순간이었습니다 :)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-cba2780e-7a4b-4241-bb89-9aa39ebcfd0d&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;SE-52b5293e-4202-4946-b790-e90136b98074&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 배포 후 예상치 못하는 상황이 생길 수 있던 문제&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;MVP 배포 전, 프론트 서버와 백엔드 서버가 미리 배포된 상태에서 서로 연결이 잘 되는지 등을 테스트해보면 좋겠다는 생각이 들었으나, 이런저런 이슈들로 배포 일정이 조금씩 연기되며 해당 테스트들을 여유있게 할 수 없던 상황이 됐습니다. 대표적으로 예상되는 문제로는 프론트에서 백엔드 서버로 HTTP 요청이 안 간다든지, S3 이미지들이 로딩이 안 된다든지, CORS 문제가 생긴다든지 등이 있었습니다. 또한 백엔드 프로젝트는 local 프로필에서는 H2 DB를 사용하도록 설정한 것 등이 있었기 때문에 prod 프로필에 대한 설정을 다 마쳤는지 등도 배포 시에 점검이 필요한 상황이었습니다. 그래서 &lt;/span&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;아예 이런 이슈들이 발생할지를 체크리스트로 만들어 활용하면 좋을 것 같아서 별도 문서로 만들어서 공유했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2546&quot; data-origin-height=&quot;2074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/APFxM/btsQdenZnkm/pJz0oK7bBJAvtunhAKMCYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/APFxM/btsQdenZnkm/pJz0oK7bBJAvtunhAKMCYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/APFxM/btsQdenZnkm/pJz0oK7bBJAvtunhAKMCYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAPFxM%2FbtsQdenZnkm%2FpJz0oK7bBJAvtunhAKMCYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2546&quot; height=&quot;2074&quot; data-origin-width=&quot;2546&quot; data-origin-height=&quot;2074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 다행히(?) MVP 배포 후 이미지가 로딩이 안 된다든지 등 설정 미흡으로 인한 이슈는 없었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 아무래도 제가 평일 낮에는 회사에 있다보니 이때 이슈가 터지면 대응이 어렵겠다는 생각이 들어, ERROR 레벨의 로그는 디스코드를 통해 발송하도록 추가 설정을 개발해서 적용해뒀습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;1420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1tHu1/btsQeEyOs5f/WzhjfJxhpYKA0kkuEVUaz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1tHu1/btsQeEyOs5f/WzhjfJxhpYKA0kkuEVUaz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1tHu1/btsQeEyOs5f/WzhjfJxhpYKA0kkuEVUaz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1tHu1%2FbtsQeEyOs5f%2FWzhjfJxhpYKA0kkuEVUaz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2590&quot; height=&quot;1420&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;1420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음 번에 이렇게 하면 좋겠다 싶은 점, 이런 거 있으면 좋겠다 싶은 것들&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. PR 리뷰 자동화&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 저희 팀 프론트 분들이 코드래빗이라는 서비스를 통해 PR을 자동화한 걸 보고 &quot;오 이거 괜찮은데?&quot;라는 생각이 들었습니다. 물론 돈을 내야 하는 서비스긴 하나 그럴 만한 가치가 있다는 생각이 들었는데요, 다음에 프로젝트를 할 때는 초기에 이런 거를 제안해서 세팅해서 사용하면 괜찮다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 좀 더 효율적인 API 명세 작성과 공유 방법&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아무래도 개발한 것이 당장 없는 상태에서 API 명세서를 먼저 만들기 위해 노션을 통해 명세 문서를 만든 다음 AI를 통해 OpenAPI 스펙의 yaml파일로 변환하여 사용했는데, 이 과정에서 좀 더 효율적인 방법은 없었을까 라는 생각이 듭니다. 프로젝트 설정을 하는 초기에 빠르게 yaml파일을 구축하는 방법이 있을지 좀 더 고민해야 할 것 같네요.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 테크리더의 필요성&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아무래도 개발자들 각각이 기획팀과 소통하는 것보다는 개발자들 의견과 상황을 취합해서 기획팀과 소통하는 구조가 효율성 측면에서 더 나을 것 같다는 생각이 중간중간 든 적이 있어서, 그런 역할을 하는 테크리더가 있으면 어떨까라는 생각이 들었습니다. 백엔드 파트 리더, 프론트 파트 리더로 분리해도 될 것 같기도 하네요. 다만 오히려 테크리더를 거쳐서 의견이 전달되는 구조가 안 좋게 작용할 수도 있겠다는 생각이 한편으로 들기도 합니다. 그래도 효율성 측면에서의 장점이 있긴 할 것 같아서 다음 번에 프로젝트를 하게 되면 초기 단계에서 테크리더를 선출해보는게 어떻냐는 의견을 내보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;감상&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;간만에 사이드 프로젝트 동아리에 참가해서 퇴근 후에도 시간을 투자해서 진행하고 있습니다. 기술 역량 향상, 일 굴리는 방법 습득 등의 목적(?)이 있어서 신청했기 때문에 최대한 적극적으로 하고 있는데, 사이드 프로젝트라는 특성 상 녹록치 않은 점들도 분명 있는 것 같습니다. 하지만 그런 과정에서도 기술적으로 안 해본 기술을 사용하며 배우는 것도 있고, &quot;아 이렇게 해보니까 좋네&quot;, &quot;아 이렇게 하는 것보다 이렇게 하는 게 낫네&quot; 등 일을 굴리는 관점에서도 배우는 것들이 있어서 개인적으로 만족하고 있습니다. 현재 고도화 기간이기 때문에 최대한 고도화 기간에 집중하고, 내년에도 기회가 된다면 사이드 프로젝트를 해보고 싶다는 생각입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※ &lt;/span&gt;이 활동은 젝트에서 진행한 프로젝트에 대한 회고입니다. 관심 있으신 분들은 둘러보세요.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;120&quot; data-origin-height=&quot;40&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/du41Il/btsQfzjyPF2/fcezw0uk18ZT5GF7mJlSF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/du41Il/btsQfzjyPF2/fcezw0uk18ZT5GF7mJlSF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/du41Il/btsQfzjyPF2/fcezw0uk18ZT5GF7mJlSF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdu41Il%2FbtsQfzjyPF2%2Ffcezw0uk18ZT5GF7mJlSF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;120&quot; height=&quot;40&quot; data-origin-width=&quot;120&quot; data-origin-height=&quot;40&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://ject.kr&quot;&gt;https://ject.kr&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figure id=&quot;og_1756566789255&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;젝트(JECT)는 다양한 포지션 멤버들과 협업할 수 있는 IT 사이드 프로젝트 동아리예요. 홈페이지에서 더 자세한 내용을 확인해보세요!&quot; data-og-host=&quot;ject.kr&quot; data-og-source-url=&quot;https://ject.kr&quot; data-og-url=&quot;https://ject.kr/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://ject.kr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ject.kr&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;젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;젝트(JECT)는 다양한 포지션 멤버들과 협업할 수 있는 IT 사이드 프로젝트 동아리예요. 홈페이지에서 더 자세한 내용을 확인해보세요!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ject.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>생각정리, 일상</category>
      <category>개발자 회고</category>
      <category>젝트</category>
      <category>젝트 3기</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/149</guid>
      <comments>https://jofestudio.tistory.com/149#entry149comment</comments>
      <pubDate>Sun, 31 Aug 2025 00:23:50 +0900</pubDate>
    </item>
    <item>
      <title>어설프게 알면 당하는 MySQL REPEATABLE_READ의 함정</title>
      <link>https://jofestudio.tistory.com/148</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;BE 개발자라면 DB를 공부하면서 트랜잭션 격리 수준(Transaction Isolation Level)을 접한 적이 있을 겁니다. 저 역시 취준 시절 기술 면접을 준비하면서, 그리고 DB를 공부하면서 트랜잭션 격리 수준을 공부한 적이 있는데요. 사실 공부한 적만 있지 사이드 프로젝트 또는 실무에서 트랜잭션 격리 수준으로 인한 문제를 마주쳤던 적은 없었습니다. 하지만 이번에 사내에서 낙관적 락으로 동시성 문제를 방지하는 기능을 개발하는 과정에서 트랜잭션 격리 수준에 대해 어설프게 알고 있던 덕에 뜻밖의 곤욕을 치르게 됐는데요. 아무래도 저처럼 이론으로만 트랜잭션 격리 수준을 공부했던 분들에게 도움이 될 수도 있는 내용인 것 같아 공유하고자 이 글을 쓰게 됐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하기 내용은 트랜잭션 격리 수준, 그 중에서도 REPEATABLE_READ에 대한 지식이 있음을 바탕으로 작성했습니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;보안 문제 등으로 제가 담당한 시스템의 정확한 명칭과 세부 업무, 기능은 언급하지 않고자 했습니다. 이 점 양해 부탁드립니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;개발 과정 &amp;amp; 고민했던 점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 이번에 개발한 기능은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;통보서 중복 작성 방지&quot;&lt;/b&gt;&lt;/span&gt; 기능이었습니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존 통보서 작성 프로세스는 다음과 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;통보서를 보낼 업무(BUSINESS_INFO 테이블의 레코드)에 대해 특정 프로세스 진행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;고객에게 보낼 통보서 내용을 DB에 INSERT ( = 통보서 작성)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 업무의 통보상태를 NOT_REGISTER(통보 내용 미작성)에서 REGISTER(통보 내용 작성)으로 UPDATE&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 비즈니스 로직은 코드 상에서는 다음처럼 구현되어 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748535650354&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void processNotice(String bizSeqNbr) {
    // 통보서를 보낼 업무 정보 조회
    BusinessInfo businessInfo = readBusinessInfo(bizSeqNbr);

    // .. 생략 (특정 프로세스 진행)

    // 고객에게 보낼 통보서 내용 INSERT
    insertNotice(notice);

    // 해당 업무의 통보상태를 UPDATE
    updateBusinessNoticeStatus(bizSeqNbr, NoticeStatus.REGISTER);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748622941425&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- updateBusinessNoticeStatus

UPDATE BUSINESS_INFO
SET NOTICE_STATUS    = #{BUSINESS_NOTICE_STATUS}
  , LAST_CHANGE_ID   = #{EMPNO}
  , LAST_CHANGE_DTTM = NOW()
WHERE BUSINESS_SEQUENCE_NUMBER = #{BIZ_SEQ_NBR};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;통보서 작성 후 해당 업무의 통보상태&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;(BUSINESS_INFO.NOTICE_STATUS)를 REGISTER로 갱신하므로, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;중복 작성을 방지하는 가장 쉬운 방법은 해당 업무의 통보상태 값이 REGISTER라면&amp;nbsp; 통보서를 작성하지 않도록 하는 것이라고 생각했습니다. 하지만 단순히 이 상태 조회 체크 로직만 둔다면 동시성 문제가 발생할 수 있으니 이를 막기 위해서는 업무 정보를 조회하는 시점부터 비관적 락을 적용하는 방법도 있었으나, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;이 시스템은 소수의 직원이 사용하는 사내 시스템이었기 때문에 중복 작성이 흔하게 일어나진 않을 거라 판단&lt;/b&gt;&lt;/span&gt;하여 다음과 같은 낙관적 락 형태를 적용한 쿼리를 활용하기로 했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749391945996&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- updateBusinessNoticeStatusToRegister

UPDATE BUSINESS_INFO
SET NOTICE_STATUS    = #{BUSINESS_NOTICE_STATUS}
  , LAST_CHANGE_ID   = #{EMPNO}
  , LAST_CHANGE_DTTM = NOW()
WHERE BUSINESS_SEQUENCE_NUMBER = #{BIZ_SEQ_NBR}
AND NOTICE_STATUS = 'NOT_REGISTER'; 

-- version 등의 컬럼을 추가할 수 없던 상황이라.. 통보서 작성 전의 통보상태는 NOT_REGISTER임을 활용
-- 갱신된 레코드가 0개라면 예외를 일으켜 롤백&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 사용 중인 DB(=MySQL)은 트랜잭션 격리 수준으로 디폴트인 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;REPEATABLE_READ&lt;/b&gt;&lt;/span&gt;를 사용중이었습니다. 동일 트랜잭션에서는 여러 번 SELECT를 해도 항상 같은 결과가 나오도록 보장하는 격리 수준으로, 트랜잭션 시작 후 최초로 읽기 시작한 시점의 스냅샷을 참조하는 원리인데요. 저는 이 스냅샷을 UPDATE 시에도 동일하게 활용할 것이라고 생각해서, 다음처럼&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; 낙관적 락을 적용해도 통보서를 중복으로 작성되는 문제가 생길 수 있다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2510&quot; data-origin-height=&quot;1352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bX5XPq/btsOsckHxWA/w49Ml3hMnQFKxsJzXOj9IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bX5XPq/btsOsckHxWA/w49Ml3hMnQFKxsJzXOj9IK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bX5XPq/btsOsckHxWA/w49Ml3hMnQFKxsJzXOj9IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbX5XPq%2FbtsOsckHxWA%2Fw49Ml3hMnQFKxsJzXOj9IK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2510&quot; height=&quot;1352&quot; data-origin-width=&quot;2510&quot; data-origin-height=&quot;1352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 여러 번의 테스트 결과, &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;중복 갱신은 발생하지 않았습니다.&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; 벌어지지 않을 일에 대해 &quot;헉 이거 문제 생기는 거 아냐?&quot;라고 생각하고 있던 거죠.. 근데 왜 발생하지 않았을까요? 저는 트랜잭션 안에서 SELECT와 UPDATE가 같은 시점의 데이터(스냅샷)를 바라보고 수행된다고 생각했지만, &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;실제로는 SELECT는 과거의 스냅샷을 바라보고 UPDATE는 현재를 바라보고 수행되기 때문이었습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;어설프게 알면 당하는 MySQL REPEATABLE_READ의 함정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 현상은 MySQL 공식 문서에서도 찾아볼 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;A consistent read means that&amp;nbsp;InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point in time, and no changes made by later or uncommitted transactions. The exception to this rule is that the query sees the changes made by earlier statements within the same transaction. This exception causes the following anomaly: If you update some rows in a table, a SELECT&amp;nbsp;sees the latest version of the updated rows, but it might also see older versions of any rows. If other sessions simultaneously update the same table, the anomaly means that you might see the table in a state that never existed in the database.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;일관된 읽기(Consistent Read)란 InnoDB가 MVCC을 사용해, 쿼리에 특정 시점의 DB 상태를 스냅샷으로 제공하는 것.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;313&quot; data-start=&quot;221&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쿼리는 해당 시점 이전에 커밋된 트랜잭션의 변경분은 볼 수 있지만, 그 이후에 커밋되었거나 아직 커밋되지 않은 트랜잭션의 변경분은 볼 수 없다. (= REPEATABLE_READ의 원리)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;313&quot; data-start=&quot;221&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;단, 같은 트랜잭션 내에서 수행된 DML(INSERT, UPDATE 등)로 인한 변경분은 볼 수 있다&lt;/b&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;452&quot; data-start=&quot;403&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 때문에 아래와 같은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;비일관성(anomaly) 이 발생 가능&lt;/b&gt;&lt;/span&gt;하다.&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;565&quot; data-start=&quot;459&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;같은 트랜잭션 내에서 일부 레코드만 UPDATE한 후 SELECT를 실행하면, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;업데이트된 레코드만&lt;/b&gt;&lt;/span&gt; 최신 버전으로 보인다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;676&quot; data-start=&quot;572&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여러 트랜잭션이 동시에 동일 테이블에 대해 업데이트를 하고 있다면, 트랜잭션 내에서 SELECT로 조회되는 결과는 실제 데이터베이스에 존재한 적 없던 상태를 보여줄 수도 있다. (트랜잭션 내에서는 동일 트랜잭션의 DML로 인한 변경분만 보이므로)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;스냅샷은 트랜잭션 내의 SELECT 문에만 적용되며, DML(INSERT, UPDATE 등)에도 적용되어야 하는 건 아니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;357&quot; data-start=&quot;161&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;A 트랜잭션이 일부 레코드를 INSERT 또는 UPDATE한 뒤 커밋했을 때, 동시에 실행 중이던 REPEATABLE_READ 수준의 B 트랜잭션에서 실행되는 DELETE나 UPDATE 문은 A 트랜잭션이 방금 커밋한 행에도 영향을 줄 수 있다(&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;B 트랜잭션에서는 그 레코드들을 SELECT로 조회할 수 없더라도)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;357&quot; data-start=&quot;161&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;gt;&amp;gt; UPDATE / DELETE는 특정 시점의 스냅샷이 아닌 현재를 바라보고 수행되기 때문&lt;/b&gt;&lt;/span&gt;으로 이해할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;455&quot; data-start=&quot;364&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한 트랜잭션이 다른 트랜잭션에서 커밋한 레코드를 UPDATE하거나 DELETE할 경우에는 그로 인한 변경사항이 현재 트랜잭션 내에서 즉시 반영되어 보이게 된다. (하단 예제 이미지 참고)&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;2558&quot; data-origin-height=&quot;1504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnRNQZ/btsOwqB3fBN/qehQPLy2KUcKVKb4W2J461/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnRNQZ/btsOwqB3fBN/qehQPLy2KUcKVKb4W2J461/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnRNQZ/btsOwqB3fBN/qehQPLy2KUcKVKb4W2J461/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnRNQZ%2FbtsOwqB3fBN%2FqehQPLy2KUcKVKb4W2J461%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2558&quot; height=&quot;1504&quot; data-origin-width=&quot;2558&quot; data-origin-height=&quot;1504&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;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 토대로 정리해보겠습니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;UPDATE / DELETE는 특정 시점의 스냅샷이 아닌 현재를 바라보고 수행됩니다. 따라서 하나의 트랜잭션 안에서 SELECT한 결과를 조건으로 활용해 UPDATE / DELETE 등을 해줄 때, 이런 트랜잭션이 여러 개가 동시에 실행된다면 내가 예상한 레코드가 UPDATE / DELETE되지 않거나 내가 예상하지 않은 레코드가 UPDATE / DELETE되는, 즉 예상치 못한 동작이 발생할 수 있습니다&lt;/b&gt;&lt;/span&gt;.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 트랜잭션 격리 수준은 읽기와 관련된 문제들(Dirty Read, Non-Repeatable Read 등)들에 대한 해결책은 될 수 있으나, 쓰기에 대한 문제들(Lost Update 등)은 여전히 발생 가능합니다. 이를 위해 낙관적 락, 비관적 락(사실 끝나서 얘기하지만 저는 낙관적 동시성 제어 등으로 부르는게 더 맞다고 생각하는 편)을 적절히 적용해서 쓰기와 관련된 문제들을 해결할 수 있어야 하죠. &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다행히 제가 이번 기능 개발에서 적용한 낙관적 락 형태의 쿼리는 시스템 특성상 해당 쿼리를 활용하는 코드가 제한되어 있어 동시성 문제가 나지 않았지만, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번 삽질을 통해&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;낙관적 락 적용시에는 version등의 컬럼을 활용해 UPDATE 조건을 정확하고 유일하게 적용해야 쓰기 관련된 동시성 문제들을 깔끔하게 해결할 수 있다&lt;/b&gt;&lt;/span&gt;는 인사이트를 얻었습니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;단순한 CRUD를 넘어 격리 수준 등을 토대로 나올 수 있는 복합적인 동시성 문제들까지 고려하는 습관을 잡아야 할 것 같습니다 :)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Reference&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/59287861/repeatable-read-isolation-level-select-vs-update-where&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/59287861/repeatable-read-isolation-level-select-vs-update-where&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MySQL</category>
      <category>anomaly</category>
      <category>consistent read</category>
      <category>MySQL</category>
      <category>Repeatable Read</category>
      <category>낙관적 락</category>
      <category>트랜잭션</category>
      <category>트랜잭션 격리 수준</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/148</guid>
      <comments>https://jofestudio.tistory.com/148#entry148comment</comments>
      <pubDate>Tue, 10 Jun 2025 21:45:03 +0900</pubDate>
    </item>
    <item>
      <title>예약이체 서비스 설계해보기</title>
      <link>https://jofestudio.tistory.com/146</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;들어가며&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;작년 말, IfKakao 2024에서 카카오페이에서 지연이체 개발기란 제목의 세미나가 공개됐습니다. Kafka를 활용해 지연이체 서비스를 설계한 과정이 담겨있었는데요. 개인적으로 굉장히 흥미롭게 봤기 때문에, 직접 설계 과정을 따라가보면 왜 카카오페이에서 세미나에 녹여냈던 선택들을 했는지 더 잘 이해할 수 있고 그 과정에서 공부가 많이 되겠다는 생각이 들어, 회사 동기들과 직접 Kafka를 활용해 예약이체 서비스를 토이 프로젝트 수준에서 설계해봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 실제 이체 프로세스가 어떻게 정교하게 흘러가는지는 잘 몰랐기 때문에.. 이체 프로세스는 출금계좌의 돈을 빼고, 타행이체일 경우는 API호출을 가정하여 1초간 Thread.sleep()을 하는 수준으로만 가정하고 설계를 진행했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;본 글은 다음 목차로 진행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예약이체란?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개략적인 설계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;세부 프로세스 설계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;속도 높이기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소감&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Reference&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 예약이체란?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;그림1.png&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VcG6O/btsMMyPvab2/BjRHo2G6mi2vCc2v6xh8zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VcG6O/btsMMyPvab2/BjRHo2G6mi2vCc2v6xh8zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VcG6O/btsMMyPvab2/BjRHo2G6mi2vCc2v6xh8zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVcG6O%2FbtsMMyPvab2%2FBjRHo2G6mi2vCc2v6xh8zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;245&quot; data-filename=&quot;그림1.png&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 글에선 은행 점검 시간에 송금 건을 예약하여 은행 점검 시간 이후 자동으로 예약된 송금 건이 실행되도록 하는 것을 말하도록 하겠습니다. 참고로 송금 예약을 눌렀을 때 어떻게 저장하는지는 여기서 다루지 않고, 저장된 예약이체 건을 실행하는 것에 대한 아키텍처와 프로세스만을 다뤘습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 개략적인 설계&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음 내용들에 대한 개략적인 설계입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터 스키마 설계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;API 설계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개략적인 아키텍처와 프로세스&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 데이터 스키마 설계&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;예약이체 테이블 (scheduled_transfer)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style8&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;필드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;자료형&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;scheduled_transfer_id (PK)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예약이체 건 식별자&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;bigInt&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;from_account&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;출금계좌(source)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;varchar(20)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;to_account&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;송금계좌(destination)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;varchar(20)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;to_bank_code&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;송금은행코드&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;varchar(10)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;transfer_amount&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;송금금액&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;decimal(15, 2)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;transfer_available_dttm&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;송금가능일시&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;datetime&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;status&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예약이체 건 상태&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;tinyInt&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;scheduled_dttm&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예약일시&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;datetime&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예약이체 건 상태값으론 0, 1, 2 등이 저장되며 각각 PENDING, COMPLETED, FAILED 등을 의미합니다. 신규 상태값이 추가될 수도 있음을 고려해 enum이 아닌 tinyInt타입을 사용하도록 했습니다. 마지막 수정일시 등의 컬럼도 필요하겠으나 위 표에서는 생략했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;고객 정보, 계좌 정보, 은행 정보에 대한 스키마는 아주 간단한 수준(식별자, 고객명, 은행코드, 은행명 등만 있는 수준)으로만 설계했으므로 따로 작성하진 않겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2) API 설계&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 말씀드렸듯 예약이체 건을 저장하는 프로세스는 다루지 않았기 때문에, 예약이체 건을 저장하는 API는 설계하지 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;POST /v1/scheduled-transfers/:id/execute&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예약 이체 건을 실행하는 API입니다. id는 예약이체 건의 식별자이며, API 호출 시 body에 담아 전달하는 인자들은 다음처럼 설계했습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;필드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;자료형&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;from_account&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;출금계좌(source)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;varchar(20)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;to_account&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;송금계좌(destination)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;varchar(20)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;to_bank_code&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;송금은행코드&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;varchar(10)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;transfer_amount&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;송금금액&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;decimal(15, 2)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3) 개략적인 아키텍처와 프로세스&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가장 초기에 구상한 아키텍처와 프로세스는 다음과 같습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;1277&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJJLCQ/btsMM7c6i6s/ThK7PxGVAQpaxkaBMeDrA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJJLCQ/btsMM7c6i6s/ThK7PxGVAQpaxkaBMeDrA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJJLCQ/btsMM7c6i6s/ThK7PxGVAQpaxkaBMeDrA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJJLCQ%2FbtsMM7c6i6s%2FThK7PxGVAQpaxkaBMeDrA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1277&quot; height=&quot;546&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;각 컴포넌트별 역할 및 프로세스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스케쥴러(Producer) : scheduled_transfer 테이블에 PENDING상태로 있는 예약이체 건들 중 송금시간이 다가온 것들(송금가능일시 &amp;lt;= 현재시간)을 5분 주기로 조회하여 Kafka로 발행하는 역할을 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Kafka : 스케쥴러가 발행한 예약이체 건들에 대한 브로커 역할을 합니다. 예약이체 건들은 scheduled-transfer라는 토픽에 저장되며, 해당 토픽은 파티션 3개로 구성했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Consumer : Kafka에 발행된 예약이체 건들을 가져와서 코어뱅킹 서버로 이체 실행 요청을 보내는 역할을 합니다. 각 Consumer들은 Kafka 파티션들과 1 : 1로 대응되도록 3개로 구성했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코어뱅킹 서버 : Consumer로부터 요청받은 예약 이체 건을 실행하는 내부 서버 역할을 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;완료된 예약 이체 건의 상태 갱신은 누가 할까?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;초기엔 단순하게 Consumer에서 코어뱅킹 서버로 이체 실행 요청을 보낸 후, 응답값에 따라 Consumer에서 예약 이체 건들의 상태를 갱신하는 프로세스를 구상했습니다. 그러나 다음과 같은 케이스들이 발생할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코어뱅킹 서버에서 이체 실행은 완료됐으나 네트워크 오류 등으로 Consumer로 응답을 못 준 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코어뱅킹 서버로부터 응답은 왔으나 Consumer에서 예약 이체 건의 상태 갱신에 실패하는 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;등등..&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 상황에서는 예약 이체 건의 상태를 갱신할 수 없으니, 실제로 이체가 실행됐어도 스케쥴러가 계속해서 kafka로 예약 이체 건들을 발행해주는 문제가 발생할 수 있다고 생각했습니다. 예약 이체 테이블(scheduled_transfer)과 계좌 테이블들이 같은 DB에 있었기 때문에, 코어뱅킹 서버에서 이체 실행과 예약 이체 건의 상태 갱신을 하나의 DB 트랜잭션에서 처리하도록 설계하여 위 문제를 해결할 수 있겠다는 생각이 들었습니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;그러나 실무 상황이라면 예약 이체 테이블과 계좌 테이블이 다른 DB에 있다든가, 코어뱅킹 서버에서 예약 이체 테이블이 있는 DB로 접근할 수 없다든가 하는 제약들이 따를 수 있습니다&lt;/b&gt;&lt;/span&gt;. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서 코어뱅킹 서버에서 예약이체 건 식별자(scheduled_transfer_id)에 대한 멱등 처리를 해줘서 기 실행됐던 예약 이체 건에 대한 실행 요청을 받은 경우 이미 실행된 건임을 알리는 응답을 주도록 한 뒤 Consumer에서 해당 응답에 따라 예약 이체 건들의 상태를 갱신하도록 설계했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;리소스 낭비 방지를 위한 Consumer 단 필터링 프로세스 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스케쥴러가 5분 전에 Kafka로 발행한 예약 이체 건이 아직 실행되지 않았다면 스케쥴러가 동일한 예약 이체 건을 Kafka로 여러 번 발행할 수 있습니다. 물론 코어뱅킹 서버에서 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;예약 이체 건 식별자(scheduled_transfer_id)에 대한 멱등 처리를 해줘서 최대한 이체가 중복으로 발생하지 않도록 처리했으나, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;처리될 필요가 없는 예약 이체 건도 코어 뱅킹 서버로 전달되어 리소스를 낭비하게 되는 상황은 발생 가능하다고 생각했습니다&lt;/b&gt;&lt;/span&gt;.&lt;/span&gt; &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;만약 코어뱅킹 서버에서 멱등 처리가 제대로 기능하지 않게 된다면 이는 리소스 낭비에서 끝나지 않고 중복 이체 발생으로도 이어질 수 있습니다. &lt;/span&gt;따라서 Consumer에서 Kafka에서 가져온 예약 이체 건들의 PENDING 여부를 확인 후 필터링된 건들만 이체 실행 요청을 보내도록 하여 처리될 필요가 없는 건들을 필터링하도록 설계했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;2414&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVDVVW/btsMMGz1EZS/fSdKdVkHrCIdrBIntt72zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVDVVW/btsMMGz1EZS/fSdKdVkHrCIdrBIntt72zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVDVVW/btsMMGz1EZS/fSdKdVkHrCIdrBIntt72zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVDVVW%2FbtsMMGz1EZS%2FfSdKdVkHrCIdrBIntt72zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2414&quot; height=&quot;1162&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 세부 프로세스 설계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 여전히 존재하는 중복 이체의 가능성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스케쥴러가 Kafka로 동일한 예약 이체 건을 중복으로 쌓아두는 상황은 여전히 가능한 상황이었습니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Consumer에서 상태가 PENDING인지 체크하는 과정을 추가하여 처리할 필요가 없는 예약 이체 건에 대한 리소스 낭비를 막고자 했고 코어뱅킹 서버에서도 멱등 처리를 해주긴 했으나, 만약 동일한 예약이체 건들이 서로 다른 Consumer에서 동시에 실행된다면 다음과 같이 중복으로 이체가 발생할 수 있었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2164&quot; data-origin-height=&quot;1150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eccnPP/btsP9Ke9WRC/hVKkizMgW6wmYRyYONFDR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eccnPP/btsP9Ke9WRC/hVKkizMgW6wmYRyYONFDR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eccnPP/btsP9Ke9WRC/hVKkizMgW6wmYRyYONFDR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeccnPP%2FbtsP9Ke9WRC%2FhVKkizMgW6wmYRyYONFDR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2164&quot; height=&quot;1150&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;1150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 문제를 해결하려면 동일한 예약 이체 건 여러 개가 동시에 실행되는 것을 제어할 수 있어야 한다고 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구체적인 방법으로는 다음 방법들을 구상했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;방법 1. 하나의 예약 이체 건을 정확히 한 번만 Kafka로 발행하자&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;애당초 동일한 예약 이체 건이 Kafka에 여러 번 중복으로 발행되지 않는다면 동일한 예약 이체 건들이 동시에 실행되는 상황 자체가 벌어지지 않는다는 점을 활용한 해결 방법입니다. 대표적인 구현 방법으로는 스케쥴러가 PENDING 상태인 예약 이체 건들을 조회해서 Kafka에 발행해줄 때, 해당 예약 이체 건들의 상태를 PUBLISHED로 바꿔주는 방법이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 방법은&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스케쥴러가 DB에 해당 예약 이체 건의 상태를 PUBLISHED로 갱신하는 작업&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스케쥴러가 Kafka에 해당 예약 이체 건을 발행하는 작업&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;으로 구분할 수 있으며, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;하나의 예약 이체 건을 정확히 한 번만 Kafka로 발행한다는 제약을 지키려면 두 작업의 원자성이 보장되어야 합니다&lt;/b&gt;&lt;/span&gt;. DB 트랜잭션을 통해 예약 이체 건의 상태를 PUBLISHED로 갱신한 다음 Kafka로 예약 이체 건을 발행하는 작업의 성공/실패 여부에 따라 트랜잭션을 커밋 or 롤백하면 된다고 생각했으나, 다음과 같은 상황도 충분히 발생할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1061&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY3ErW/btsMLwZAtZZ/f4oHWjs26dDcJjnVLi10TK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY3ErW/btsMLwZAtZZ/f4oHWjs26dDcJjnVLi10TK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY3ErW/btsMLwZAtZZ/f4oHWjs26dDcJjnVLi10TK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY3ErW%2FbtsMLwZAtZZ%2Ff4oHWjs26dDcJjnVLi10TK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1061&quot; height=&quot;472&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;즉 하나의 DB 트랜잭션만으로는 두 작업의 원자성을 완벽하게 보장할 수 없었습니다&lt;/b&gt;&lt;/span&gt;. 2 Phase Commit이나 트랜잭셔널 아웃박스 등을 활용하는 방법이 있겠지만, 결국 &quot;어떤 상황이 닥쳐도 하나의 예약 이체 건을 정확히 한 번만 발행해준다&quot;라는 것을 구현하기는 굉장히 어렵겠다는 생각이 들었습니다. 또한 어찌저찌 해서 정확히 한 번 발행에 성공한다고 해도 다음과 같은 상황들을 고려해야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정확히 한 번 발행된 예약 이체 건을 실행하다가 중간에 실패하면 재시도는 어떻게 할 것인가? 다시 예약 이체 건의 상태를 PENDING으로 바꾸게 할 것인가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정확히 한 번 발행된 예약 이체 건을 Consumer에서 중복으로 consume하게 되는 상황이 오면 어떻게 할 것인가? 정확히 한 번 발행됐어도, 해당 예약 이체 건을 실행하는 것도 정확히 한 번만 실행되는 것을 어떻게 처리해줄 것인가? (물론 코어뱅킹 서버에서 멱등 처리를 해주긴 했습니다만..)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;등등..&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;즉 정확히 한 번 발행에 성공해도 논리적인 관점 등에서 데이터의 일관성이 깨질 수 있는 여러 샛길들이 많다고 생각했습니다&lt;/b&gt;&lt;/span&gt;. 따라서 하나의 예약 이체 건을 정확히 한 번만 발행한다는 제약 조건을 지키기 위해 그런 샛길들을 모두 고려하며 구현하는 것보다는, 같은 이체 건 여러 개가 동시에 실행되는 것을 제어할 수 있는 다른 방법을 적용하는 것이 더 낫다고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;방법 2. 계좌 락을 걸어보자&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;두 번째로 구상한 방법은 동시성 제어에 보편적으로 많이 활용되는 &quot;락&quot;을 활용하는 것이었습니다. &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Consumer에서 예약 이체 건의 상태를 체크하는 부분부터 Consumer에서 예약 이체 건의 상태를 갱신하는 부분에 락을 걸어 같은 이체 건이 동시에 실행되는 것을 제어하자는 아이디어였습니다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예약 이체 건의 상태 체크와 상태 갱신 모두 Consumer에서 수행되고 있기 때문에, Consumer에서 예약 이체 테이블(scheduled-transfer)의 레코드에 SELECT FOR UPDATE를 통해 배타 락을 거는 방법을 우선적으로 고려했습니다. 그러나 이 방법은 락을 걸고 코어뱅킹 서버로 요청한 이체 실행의 응답이 오기까지 DB 커넥션이 늘어질 여지가 있었기 때문에, 다른 형태로 락을 걸어보자고 생각했습니다. 그 결과 예약 이체 건에 계좌 락을 거는 형태를 고려하게 됐습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;1196&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erRo9U/btsMOP4aGhs/nl7z0KfQQce8HwJOYs9GbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erRo9U/btsMOP4aGhs/nl7z0KfQQce8HwJOYs9GbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erRo9U/btsMOP4aGhs/nl7z0KfQQce8HwJOYs9GbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FerRo9U%2FbtsMOP4aGhs%2Fnl7z0KfQQce8HwJOYs9GbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;507&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;계좌 락을 얻는 방법으론 계좌 락을 위한 테이블을 별도로 만들어 활용하는 방법과 분산 락을 활용하는 방법이 있었습니다. 전자의 경우 예약 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;이체 테이블(scheduled-transfer)에 배타 락을 거는 방법과 동일하게 락을 걸고 코어뱅킹 서버로 요청한 이체 실행의 응답이 오기까지 DB 커넥션이 늘어질 여지가 있다고 판단했습니다. &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;또한&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;출금계좌에 락을 거는 것은 결국 전체 시스템에서 해당 계좌로 접근하는 스레드를 하나로 제한하기 위함이기도 한데,&lt;/span&gt; 실무에서는 계좌 정보를 하나의 DB에서 관리하지 않고 여러 시스템 또는 모듈들이 독립적인 DB를 사용 중일 수 있고 하나의 서버가 여러 서버로 분리될 수도 있기 때문에 테이블 기반의 계좌 락은 전체 시스템에서 해당 계좌로 접근하는 스레드를 하나로 제한하기엔 확장성이 낮다는 생각이 들었습니다&lt;/b&gt;&lt;/span&gt;. 따라서 분산 락을 활용해 계좌 락을 구현하는 것으로 결정했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;분산 락을 통해 같은 이체 건들이 동시에 실행되는 것을 제어하게 되는 과정을 나타내면 다음과 같게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZRoW/btsMOdEx2yK/Ya6NLq8PU0u3yELp1IB8g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZRoW/btsMOdEx2yK/Ya6NLq8PU0u3yELp1IB8g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZRoW/btsMOdEx2yK/Ya6NLq8PU0u3yELp1IB8g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZRoW%2FbtsMOdEx2yK%2FYa6NLq8PU0u3yELp1IB8g0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;941&quot; height=&quot;500&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AKaoi/btsMMLvwH4e/69A8Prt43Mg0voQnKCwyPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AKaoi/btsMMLvwH4e/69A8Prt43Mg0voQnKCwyPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AKaoi/btsMMLvwH4e/69A8Prt43Mg0voQnKCwyPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAKaoi%2FbtsMMLvwH4e%2F69A8Prt43Mg0voQnKCwyPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;967&quot; height=&quot;516&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl8toY/btsMMzWlrby/uRSMpszILdbAF5pqs7QcSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl8toY/btsMMzWlrby/uRSMpszILdbAF5pqs7QcSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl8toY/btsMMzWlrby/uRSMpszILdbAF5pqs7QcSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl8toY%2FbtsMMzWlrby%2FuRSMpszILdbAF5pqs7QcSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;962&quot; height=&quot;540&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4qbtn/btsMNLnUdc1/BUWxf1EfAt5g2L768u710K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4qbtn/btsMNLnUdc1/BUWxf1EfAt5g2L768u710K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4qbtn/btsMNLnUdc1/BUWxf1EfAt5g2L768u710K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4qbtn%2FbtsMNLnUdc1%2FBUWxf1EfAt5g2L768u710K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;550&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;550&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;984&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LDuOa/btsMOQouc4B/mHZSekFu25zZAUDeyOsl1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LDuOa/btsMOQouc4B/mHZSekFu25zZAUDeyOsl1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LDuOa/btsMOQouc4B/mHZSekFu25zZAUDeyOsl1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLDuOa%2FbtsMOQouc4B%2FmHZSekFu25zZAUDeyOsl1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;984&quot; height=&quot;550&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 예약 이체 건이 송금가능일시가 지나도 장기간동안 실행되지 않는 이슈가 발생&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 이렇게까지 해놓고 테스트를 돌려보니, 특정 예약 이체 건들이 송금가능일시가 지나도 1 ~ 2시간 이상 실행되지 않는 이슈가 발생했습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3Y7Vz/btsMQDiKZuZ/rddPXp4QKk1LIryWrC95Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3Y7Vz/btsMQDiKZuZ/rddPXp4QKk1LIryWrC95Y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3Y7Vz/btsMQDiKZuZ/rddPXp4QKk1LIryWrC95Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3Y7Vz%2FbtsMQDiKZuZ%2FrddPXp4QKk1LIryWrC95Y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1138&quot; height=&quot;649&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;원인은 위 흐름도와 같이 출금계좌는 같지만 엄연히 서로 다른 이체 건들이 동시에 실행될 때 한 쪽에선 분산 락 획득을 실패하여 이체가 실행되지 않던 상황이 생길 수 있었다는 것이었습니다. 락을 획득할 때까지 대기시키는 방법 등이 떠올랐으나 그렇게 되면 현재 구조에선 뒤에 쌓이는 예약 이체 건들이 밀릴 수 있었고, 대기시키는 방법도 이체 실행이 지연되는 건 마찬가지이니 근본적인 해결법은 아니라고 생각했습니다. 따라서 락 경합 자체를 줄일 수 있는 방법들을 고려하게 됐습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;방법 1. 분산 락 키를 바꾸자&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재는 출금계좌를 키로 해서 분산 락을 잡고 있었는데, 다른 값을 키로 잡으면 되지 않을까라는 아이디어였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ2wrT/btsMO7liEuI/VgiiCZTtO1HBuBELLijEsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ2wrT/btsMO7liEuI/VgiiCZTtO1HBuBELLijEsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ2wrT/btsMO7liEuI/VgiiCZTtO1HBuBELLijEsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ2wrT%2FbtsMO7liEuI%2FVgiiCZTtO1HBuBELLijEsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1495&quot; height=&quot;446&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;출금계좌는 각 예약 이체 건들의 고유값이 아니기 때문에 출금계좌를 분산 락 키로 잡으면 서로 다른 이체 건들끼리도 락 경합이 발생 가능합니다. 반면 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;예약 이체 건 식별자(scheduled_transfer_id)는 각 예약 이체 건들의 고유값이므로 여기에 분산 락 키를 잡아주게 되면 같은 예약 이체 건들이 동시에 실행되는 상황에 한해서만 락 경합이 발생됩니다. 따라서 락 경합을 줄이려는 목적을 달성할 수 있다는 생각이 들었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다만 앞서 말했듯 출금계좌에 락을 거는 것은 시스템 전체에서 해당 계좌로 접근하는 스레드를 하나로 제한하는 효과를 줍니다. 예약 이체 건 식별자에 대해 락을 걸면 락 경합은 분명 줄어들겠지만 해당 계좌로 접근하는 스레드가 여러 개가 될 수 있어 예상치 못한 동시성 관련 문제를 안겨줄 수 있습니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;이체는 결국 오류없이 수행되는 것이 가장 중요하다고 생각했기 때문에 분산 락 키는 출금계좌를 그대로 쓰게 하여 해당 계좌로 접근하는 스레드를 하나로 제한시켜 안전성을 높이고, 대신 락 경합을 줄일 수 있는 다른 방법을 고려해보기로 했습니다&lt;/b&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;방법 2. 예약 이체 건들을 출금계좌별로 파티셔닝하자&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;두 번째 방법은 예약 이체 건들을 출금계좌별로 같은 파티션에 가게끔 설정하는 아이디어입니다. 같은 출금계좌를 가진 서로 다른 예약 이체 건들이 동시에 실행되는 이유는 이들이 서로 다른 Consumer에서 실행될 수 있기 때문인데요. 현재 구조(3개 파티션, 3개 Consumer)에서는 각 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Consumer&lt;/span&gt;들이 서로 다른 파티션을 담당하고 있기 때문&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;(Kafka는 기본적으로 파티션 하나에 같은 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Consumer&lt;/span&gt; 그룹 내에선 단일 Consumer 스레드가 할당됨)&lt;/span&gt;에 같은 출금계좌를 갖는 예약 이체 건들을 같은 파티션으로 발행하게 되면 하나의 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Consumer&lt;/span&gt;에서만 예약 이체 건들을 처리하게 됩니다. 또한 현재 하나의 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Consumer&lt;/span&gt; 스레드에서는 담당하는 파티션에 발행된 예약 이체 건들을 순서대로 처리 중이므로 이 경우 같은 출금계좌를 갖는 예약 이체 건들이 동시에 실행되는 것이 방지되는 효과를 가져올 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Kafka Producer(스케쥴러)에서 메시지를 발행할 때 어떤 파티션으로 발행할지는 Partitioner라는 컴포넌트가 담당하는데요. kafka-clients 3.6.2 기준으로, 메시지 발행 시 키를 지정하지 않는다면 다음 그림과 같이 Sticky partitioning전략을 통해 메시지를 파티션으로 발행하게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1273&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WLA6C/btsMRn1ScxU/mtE7Qlh3HsJPJlbRHmkn80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WLA6C/btsMRn1ScxU/mtE7Qlh3HsJPJlbRHmkn80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WLA6C/btsMRn1ScxU/mtE7Qlh3HsJPJlbRHmkn80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWLA6C%2FbtsMRn1ScxU%2FmtE7Qlh3HsJPJlbRHmkn80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1273&quot; height=&quot;579&quot; data-origin-width=&quot;1273&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Sticky partitioning은 하나의 파티션을 유지하며 메시지를 발행하다가&amp;nbsp;특정 조건(배치 크기 초과, 시간 초과 등)이 충족되면&amp;nbsp;새로운 파티션을 선택하여 메시지를 발행하는 전략으로, 같은 파티션에 연속해서 메시지를 발행하므로 배치 최적화가 가능해지고&amp;nbsp;불필요한 파티션 변경을 줄여 네트워크 처리 비용 등을 감소할 수 있다는 장점이 있습니다. 다만 현재 설계 중인 예약 이체 시스템과 같이 동시에 실행되면 안 좋은 메시지들&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;(ex: 같은 출금계좌를 가진 서로 다른 예약 이체 건들)&lt;/span&gt;이 각기 다른 파티션에 분배되어 동시에 실행되는 문제를 가져다 줄 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;반면 Kafka Producer(스케쥴러)에서 메시지를 발행할 때 키를 지정하게 된다면, Partitioner는 다음과 같이 키를 기반으로 파티션을 지정하여 메시지를 발행하게 됩니다.&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;1272&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd58eB/btsMQEb3MiL/sF1sKKKfiTM5D97NY8Lgq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd58eB/btsMQEb3MiL/sF1sKKKfiTM5D97NY8Lgq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd58eB/btsMQEb3MiL/sF1sKKKfiTM5D97NY8Lgq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd58eB%2FbtsMQEb3MiL%2FsF1sKKKfiTM5D97NY8Lgq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1272&quot; height=&quot;557&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 경우 같은 출금계좌를 갖는 예약 이체건들은 같은 파티션에 할당되므로, 이들이 동시에 실행되는 상황이 방지되게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 해두고 보니.. 스케쥴러에서 동일한 예약 이체 건을 발행해도 이들은 동일한 파티션으로 할당되고(물론 리밸런싱 등이 발생하면 다른 파티션에 발행될 순 있음), 앞서 말씀드렸듯이 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;현재 하나의 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Consumer&lt;/span&gt; 스레드에서는 담당하는 파티션에 발행된 예약 이체 건들을 순서대로 처리 중이므로 동일한 예약 이체 건들이 서로 다른 Consumer에서 동시에 실행되는 상황도 없어지게 됩니다. 즉, 동시성 문제 해결을 위해 분산 락을 사용할 필요가 없어지게 됩니다. 분산 락을 획득하고 해제하는 것도 네트워크 I/O가 수반되는 것이므로, 계속해서 분산 락을 사용한다면 불필요한 리소스 낭비가 생기게 되는 셈입니다. &quot;그러면 이제 분산 락을 굳이 할 필요가 없어졌는데?&quot;라고 생각했으나, 역시나 앞서 말씀드렸듯이 출금계좌에 락을 거는 것은 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;시스템 전체에서 해당 계좌로 접근하는 스레드를 하나로 제한하는 효과를 줍니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;예약 이체 시스템이 아닌 다른 시스템에서도 해당 계좌에 접근하는 로직 등이 있을 수 있고, 이들도 분산 락을 통해 계좌 락을 걸어주고 있다면 예약 이체 시스템을 넘어서 시스템 전체에서 해당 계좌로 접근하는 스레드가 하나가 된다는 얘기입니다. 따라서 동시성 문제를 제어할 필요성이 없어지긴 했어도 시스템 전체에서의 안전성을 높이기 위해 여전히 분산 락을 잡아주는 것은 유효하다고 판단하고, 계속해서 분산 락을 사용해도 된다고 생각하게 됐습니다&lt;/b&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 속도 높이기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) Batch Read 설정&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 설정은 Consumer들이 각자가 담당하는 파티션에 발행된 예약 이체 건들을 하나 하나 가져와서 처리하고 있는데요. 이런 상황에서 처리량을 높이고 싶을 땐 대표적으로 &quot;일괄 처리&quot;를 도입해볼 수 있고 Consumer에서도 Batch read를 설정하여 파티션에 발행된 예약 이체 건들을 일괄로 가져와서 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다만 Consumer에서 특정 개수만큼 예약 이체 건들을 모을 때까지 기다렸다가 일괄로 가져가는 형태가 되기 때문에 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;각 예약 이체 건의 입장에서 보면 실시간성이 떨어질 수 있음을 고려해야 합니다. 예약 이체 시스템의 경우 송금 가능 일시가 되자마자 즉각적인 이체가 발생되어야 하는 서비스는 아니라고 판단하고 Batch Read를 도입해도 된다고 판단했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 병렬 처리 및 출금계좌별 스레드 지정&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Batch Read로 예약 이체 여러 건을 한 번에 가져와도 기본적으로는 하나의 Consumer 스레드가 이들을 처리합니다. 따라서 처리량을 높이기 위해 복수의 스레드로 이들을 병렬 처리하는 방식을 도입할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;다만 이 경우 같은 출금계좌를 가진 예약 이체 건들이 서로 다른 스레드에서 동시에 실행될 수 있고 이는 잦은 락 경합으로 이어질 수 있습니다. 이전에 출금계좌를 기준으로 특정 파티션에 발행되도록 설정했듯이, Consumer에서 출금계좌별로 처리할 스레드를 따로 지정주어 이 문제를 해결할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2500&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvn4jI/btsMTmuUSBU/TxXXZ6B7bTycaZlRhs7IwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvn4jI/btsMTmuUSBU/TxXXZ6B7bTycaZlRhs7IwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvn4jI/btsMTmuUSBU/TxXXZ6B7bTycaZlRhs7IwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvn4jI%2FbtsMTmuUSBU%2FTxXXZ6B7bTycaZlRhs7IwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2500&quot; height=&quot;1216&quot; data-origin-width=&quot;2500&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. 소감&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저랑 동기들이 설계했던 부분은 여기까지입니다. 막상 돌이켜보니 사실상 카카오페이 세미나에서 발표된 내용과 큰 차이가 없긴 합니다만.. 그래도 특정 문제(카카오페이에서 맞닥뜨린)가 생겼을 때 &quot;이런 방법도 있었을 텐데 왜 이건 쓰지 않았을까?&quot;라는 고민들이 드는 지점이 많았는데 직접 설계를 해보니 &quot;아 이래서 그랬구나&quot;라고 더 이해가 잘 되는 부분들이 많았던 것 같습니다. 예를 들면 &quot;예약 이체 건들을 계좌별로 파티셔닝되게 했다면 분산 락이 필요없어진 것 같은데 왜 안 뺐을까? -&amp;gt; 아 안전성을 위해서였구나!&quot; 등등.. 뭔가 다른 사람들이 설계하는 과정을 따라가보는 것은 처음이었는데 그 안에서 배운 것들이 굉장히 많네요. 또한 스스로 아직 많이 부족함을 느끼게 된 것 같기도 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;준비하는 과정에서도 여러 레퍼런스(개인이 올린 블로그 글들은 다 비슷한 감들이 많아.. 테크블로그에서 발행한 컨텐츠들을 최대한 참고하려고 했습니다)들을 봤습니다. 신뢰성 보장을 위해 여러 고민 끝에 기술을 도입하고, 그 기술의 도입으로 인한 리스크들도 기술적인 고민들을 가미하며 해결하는 과정들이 상당히 흥미로웠습니다. 저도 그런 엔지니어가.. 그런 전문성을 가진 사람이 되고 싶다는 생각이 드네요. 더욱,, 정진해야겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6. Reference&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. 지연이체 서비스 개발기 (카카오페이)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f2f2f2; font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://youtu.be/LECTNX8WDHo?si=8cn67Fbr4CbDtkaA&quot;&gt;https://youtu.be/LECTNX8WDHo?si=8cn67Fbr4CbDtkaA&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. 분산 시스템에서 메시지 안전하게 처리하기 (강남언니)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f2f2f2; font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://blog.gangnamunni.com/post/transactional-outbox/&quot;&gt;https://blog.gangnamunni.com/post/transactional-outbox/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3. 풀필먼트 입고 서비스에서 분산락을 사용하는 방법 (컬리)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f2f2f2; font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/&quot;&gt;https://helloworld.kurly.com/blog/distributed-redisson-lock/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;4. Kafka 메시지 중복 및 유실 케이스별 해결 방법 (올리브영)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f2f2f2; font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-10-16/oliveyoung-scm-oms-kafka/&quot;&gt;https://oliveyoung.tech/2024-10-16/oliveyoung-scm-oms-kafka/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;5. SLASH 22 - 애플 한 주가 고객에게 전달 되기까지 (토스증권)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f2f2f2; font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://youtu.be/UOWy6zdsD-c?si=iTtbPbEDpFaWdPt8&quot;&gt;https://youtu.be/UOWy6zdsD-c?si=iTtbPbEDpFaWdPt8&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/개발일지</category>
      <category>Kafka</category>
      <category>개발</category>
      <category>분산락</category>
      <category>설계</category>
      <category>예약이체</category>
      <category>트랜잭션</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/146</guid>
      <comments>https://jofestudio.tistory.com/146#entry146comment</comments>
      <pubDate>Fri, 21 Mar 2025 22:53:05 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭션, ACID, 트랜잭션 격리 수준, 낙관적 동시성 제어와 비관적 동시성 제어</title>
      <link>https://jofestudio.tistory.com/145</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 트랜잭션이란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;데이터베이스 관리 시스템 내의 논리적인 작업 단위를 말합니다. 10,000원 단위로 돈을 내라고 하는데 5,000원을 내면 안되는 것처럼, 작업 단위라는 말은 이 작업이 반만 실행되는 일은 없어야 한다는 것, 다시 말하면 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;nbsp;전체적으로 실행되거나 전혀 실행되지 않아야 한다는 것을 뜻&lt;/b&gt;&lt;/span&gt;합니다. 즉 트랜잭션은&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt; 데이터베이스가 처리해야 하는 일종의 작업 시나리오인데, 데이터베이스의 일관성과 신뢰성을 보장하는 All or Nothing의 시나리오를 말하는 것으로 이해할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;일관성&lt;/b&gt;&lt;/span&gt; : &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜잭션 전후로 데이터베이스 내 데이터는 무결해야 한다&lt;/b&gt;&lt;/span&gt;는 것, 즉 데이터베이스 내 데이터들은 어떤 순간에도 정확하고 완전해야 하며 이 데이터를 신뢰할 수 있어야 한다는 것을 말합니다. 예를 들어 같은 DB에 있는 A계좌에서 B계좌로 돈을 보냈는데, A계좌에 출금 기록은 있지만 B계좌에 입금 기록이 없다면 이 DB는 일관된 상태로 볼 수 없습니다.(참고로 이 케이스는 명시되지 않은 제약 조건으로 볼 수 있으며, 반대로 명시적인 제약 조건으로는 외래 키 제약 등이 있습니다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;신뢰성&lt;/b&gt;&lt;/span&gt; : 데이터베이스 내 데이터가 안전하게 저장되고 정확하게 관리되어, 시스템 장애나 오류 상황에서도 데이터가 손실되거나 손상되지 않도록 보장하는 특성을 말합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;트랜잭션은 보통 다음과 같은 과정으로 진행됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;트랜잭션 시작(BEGIN)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;작업 수행(데이터 read/write 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일련의 과정들이 문제없이 수행됐다면 COMMIT (작업 내용이 DB에 영구저장됨)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;중간에 문제가 발생했다면 ROLLBACK (트랜잭션 시작 전으로 되돌아감)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ex)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734853038722&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 트랜잭션 시작
BEGIN;

-- 작업 1
UPDATE people
SET name = 'Ray'
WHERE id = 1;

-- 작업 2
UPDATE people
SET lastname = 'Cho'
WHERE id = 10;

-- 일련의 작업이 문제없이 진행되면 DB에 반영
COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;2. 트랜잭션의 ACID란?&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;데이터베이스의 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;일관성&lt;/b&gt;&lt;/span&gt;과 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;신뢰성&lt;/b&gt;&lt;/span&gt;을 위해, 앞서 소개한 All or Nothing을 포함해 트랜잭션은 ACID라고 불리는 4가지 성질을 가져야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;1) Atomicity (원자성)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜잭션으로 처리할 작업 시나리오는 모두 처리되든지 아니면 하나도 처리되지 말아야 하든지에 대한 것&lt;/b&gt;&lt;/span&gt;으로, 앞서 설명한 All or Nothing에 해당합니다. COMMIT 실행 시 작업 내용을 DB에 쓰는 것과 ROLLBACK 실행시 DB를 되돌리는 것은 DBMS가 해주는 부분이므로, 개발자는 트랜잭션을 만들면서 언제 COMMIT을 하고 어떤 경우에 ROLLBACK을 할 지를 잘 챙겨야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;2) Consistency (일관성)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜잭션 처리 전후로 DB는 일관된 상태를 유지해야 한다는 것&lt;/b&gt;&lt;/span&gt;을 말합니다. 물론 뜯어보면 트랜잭션 처리 전과 후는 데이터베이스가 다른 상태이겠지만, 어쨌든 둘 다 똑같이 일관된 상태를 유지해야 한다는 뜻입니다. 외래 키 또는 CHECK문을 통한 명시적인 제약을 위반했는지는 DBMS가 COMMIT 전에 확인하고 알려주지만, Application 관점에서 트랜잭션이 일관성을 보장할 수 있도록 하기 위해서는 개발자가 트랜잭션을 잘 정의할 필요가 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;3) Isolation (독립성)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;데이터베이스에서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;여러 트랜잭션이 동시에 수행되어도 각 트랜잭션들이 서로 영향을 주지 않아야 함&lt;/b&gt;&lt;/span&gt;을 말합니다. Atomicity(원자성)는 어떤 트랜잭션을 처리하기 전까지 다른 트랜잭션을 처리하지 않는다는 개념이 아니라 단순히 &quot;작업&quot;이라는 관점에서 이 작업이 모두 수행되냐 아니냐의 개념입니다. 따라서 여러 트랜잭션이 동시에 실행될 수 있고, 이로 인한 동시성 문제들이 발생할 수 있기 때문에 트랜잭션들을 각각 따로따로 실행되는 것처럼 독립시켜 각 트랜잭션들이 서로 영향을 주지 않게 할 필요가 있습니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;이를 만족하는 가장 쉽고 강력한 방법은 모든 트랜잭션을 직렬로 수행하는 것이나 이는 동시성이 감소하므로 DB 퍼포먼스에 심각한 악영향을 끼칩니다. 따라서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;DBMS는 보통 여러 종류의 Isolation Level을 만들어 트랜잭션들이 서로 영향을 주는 정도를 설정할 수 있도록 제공&lt;/b&gt;&lt;/span&gt;합니다. 이를 통해 개발자는 어떤 Level로 트랜잭션을 동작시킬 지 설정할 수 있으며, 각 Level에서 발생 가능한 문제들을 인지하고 다룰 수 있어야 합니다. (아래에서 상세히 다뤄보겠습니다)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;4) Durability (영속성)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜잭션이 완료되면 그 결과는 영구적으로 저장되어야 함&lt;/b&gt;&lt;/span&gt;을 말합니다. 시스템이 고장 나더라도 트랜잭션의 결과는 손실되지 않아야 하며, 데이터베이스에 영구적으로 반영되어야 함을 뜻합니다. 기본적으로 DBMS가 보장합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 트랜잭션 동시 실행으로 발생 가능한 대표적인 문제들&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) Dirty Read&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;COMMIT되지 않은 변화분을 읽는 문제&lt;/b&gt;&lt;/span&gt;를 말합니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가령 x에 10, y에 10이 저장되어 있을 때&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;font-family: 'Noto Sans Light';&quot;&gt;Tx A : X에 Y를 더함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx B : Y에 10을 더함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;을 한다고 가정해보겠습니다. 어떠한 사유로 인해 Tx B가 롤백되는 케이스 중 다음과 같은 케이스가 발생 가능합니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(아래 이미지에서 initial Data의 y = 10입니다)&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;889&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JykCh/btsLtbnBHlF/OqcxrhhogrV7OzG8ny67nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JykCh/btsLtbnBHlF/OqcxrhhogrV7OzG8ny67nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JykCh/btsLtbnBHlF/OqcxrhhogrV7OzG8ny67nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJykCh%2FbtsLtbnBHlF%2FOqcxrhhogrV7OzG8ny67nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;521&quot; data-origin-width=&quot;889&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx A 입장에선 y값을 20으로 읽어서 최종적으로 x에 30을 write했습니다. 그러나 Tx B가 마지막에 롤백되어 Tx A는 유효하지 않은 y값 20을 읽었던 셈이 됐고 결과적으로 x에 write했던 30도 유효하지 않은 값이 됐습니다. 이 현상을 Dirty Read라고 부르며 일관되지 않은 결과를 초래하는 원인이 될 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) Non-Repeatable Read&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;한 트랜잭션에서 어떠한 데이터를 두 번 이상 읽을 때 각 읽기 결과가 달라지는 것&lt;/b&gt;&lt;/span&gt;을 말합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가령 x에 10이 있을 때&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;font-family: 'Noto Sans Light';&quot;&gt;Tx A : x를 한 번 읽고 어떤 로직을 처리하다가 x를 한 번 더 읽음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx B : x에 20을 더함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;을 한다고 가정해보겠습니다. Tx A에서 진행되는 두 번의 읽기 사이에 Tx B가 실행&amp;amp;COMMIT되면 다음 현상이 발생 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;996&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DmVdy/btsLtpzdhWk/UKu6BVgQ2KHAN3zHTkjvO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DmVdy/btsLtpzdhWk/UKu6BVgQ2KHAN3zHTkjvO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DmVdy/btsLtpzdhWk/UKu6BVgQ2KHAN3zHTkjvO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDmVdy%2FbtsLtpzdhWk%2FUKu6BVgQ2KHAN3zHTkjvO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;833&quot; height=&quot;560&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx A 입장에선 같은 데이터인 x를 한 트랜잭션에서 두 번 읽었는데 결과가 달라졌습니다. DB 관점에서 보면 Tx B가 실행되기 전후에 맞춰 데이터를 잘 읽은 것으로 볼 수도 있습니다. 그러나 Application 관점에서 보면 첫 번째 읽기를 통해 처리한 로직이 왜곡되든가 중복 처리가 되는 현상 등이 발생 가능합니다. 즉 &quot;두 번의 읽기 결과값이 달라지는 것이 문제&quot;라기보다는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;트랜잭션이 시작된 후 읽은 데이터를 신뢰할 수 없게 된다는 것이 문제&quot;&lt;/b&gt;&lt;/span&gt;라고 봐야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) Phantom Read&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;한 트랜잭션에서 같은 조건의 데이터를 여러 번 읽을 때 각 읽기 결과가 달라지는 것(읽었던 행이 없어지거나 새 행이 읽히는)&lt;/b&gt;&lt;/span&gt;을 말합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가령 v라는 컬럼값을 각각 10, 20으로 갖는 x1과 x2라는 레코드가 있을 때&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;font-family: 'Noto Sans Light';&quot;&gt;Tx A : v=10인 레코드를 한 번 읽고 어떤 로직을 처리하다가 같은 조건으로 레코드를 한 번 더 읽음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx B : x2의 v를 10으로 바꿈&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;을 한다고 가정해보겠습니다. Tx A에서 진행되는 두 번의 읽기 사이에 Tx B가 실행&amp;amp;COMMIT되면 다음 현상이 발생 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1170&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnmBnv/btsLtHs0E8z/vjnYhqD7TuLwy18dzWrlc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnmBnv/btsLtHs0E8z/vjnYhqD7TuLwy18dzWrlc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnmBnv/btsLtHs0E8z/vjnYhqD7TuLwy18dzWrlc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnmBnv%2FbtsLtHs0E8z%2FvjnYhqD7TuLwy18dzWrlc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;643&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한 트랜잭션에서 같은 조건으로 데이터를 읽었는데 첫 번째 읽기에선 없던 레코드가 두 번째 읽기에서 생겼습니다. 만약 Tx B에서 x1을 지웠다면 Tx A의 두 번째 읽기에선 어떤 레코드도 잡히지 않을 텐데 이것도 Phantom Read입니다. Non-Repeatable Read와 비슷하게 DB 관점에서 보면 Tx B가 실행되기 전후에 맞춰 데이터를 잘 읽은 것으로 볼 수 있으나, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Application 관점에서 보면 첫 번째 읽기를 통해 처리한 로직이 왜곡되는 현상 등이 발생 가능합니다. 이 역시도 &quot;두 번째 읽기에서 새로운 행이 추가되거나 읽었던 행이 삭제되는 게 문제&quot;라기보다는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;트랜잭션이 시작된 후 특정 조건으로 읽은 결과를 신뢰할 수 없게 되는게 문제&quot;&lt;/b&gt;&lt;/span&gt;라고 봐야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;4. 트랜잭션 격리 수준 (Transaction Isolation Level)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;위에서 살펴봤던 동시성에 관련된 문제들을 방지하려면 각 트랜잭션들이 서로에게 영향을 주지 않도록 격리해야 하나, 그 정도를 강하게 할 경우 동시성이 낮아지게 됩니다. 즉 동시에 처리 가능한 트랜잭션 수가 줄어드는 것이고 이는 곧 DB 퍼포먼스 저하로 이어집니다. 그래서 DBMS는 보통 여러 Isolation Level을 만들어 트랜잭션이 서로 영향을 주는 정도를 개발자들이 선택할 수 있도록 해뒀습니다.  SQL 표준에서 정의한 Isolation Level에선 가장 낮은 레벨이 Read Uncommitted, 가장 높은 레벨이 Serializable로 레벨이 높아질수록 동시성이 낮아지나 격리 정도가 높아지고, 레벨이 낮아질수록 동시성이 높아지나 격리 정도가 낮아집니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;1) Read Uncommitted&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜잭션 실행 중에 다른 트랜잭션에서 커밋되지 않은 데이터도 읽을 수 있도록 하는 레벨&lt;/b&gt;&lt;/span&gt;입니다. 사실상 트랜잭션 격리를 하지 않는 레벨로 Dirty Read, Non-Repeatable Read, Phantom Read를 비롯한 모든 동시성 문제가 발생 가능합니다. 다만 동시성은 가장 높은 레벨입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;2) Read Committed&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜잭션 실행 중엔 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있도록 하는 레벨&lt;/b&gt;&lt;/span&gt;입니다. Dirty Read 문제가 발생되지 않으나 Non-Repeatable Read, Phantom Read 문제는 여전히 발생 가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;3) Repeatable Read&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SELECT로 읽은 레코드에 락을 거는 방법 등을 통해 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;동일 트랜잭션 실행 중 한 레코드를 여러 번 읽어도 항상 같은 결과가 나오도록 보장하는 레벨&lt;/b&gt;&lt;/span&gt;입니다. Dirty Read, Non-Repeatable Read 문제는 발생되지 않으나 Phantom Read 문제는 여전히 발생 가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;4) Serializable&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;트랜잭션이 읽거나 쓰는 모든 데이터에 락을 거는 방법 등을 통해 &lt;b&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜잭션들을 직렬로 실행하는 것처럼 보이게 하는 레벨&lt;/b&gt;&lt;/span&gt;입니다. Dirty Read, Non-Repeatable Read, Phantom Read를 비롯한 모든 동시성 문제가 발생되지 않는 레벨입니다. 다만 동시성이 가장 낮습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;표로 정리하면 다음과 같습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Dirty Read&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Non-Repeatable Read&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Phantom Read&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Read Uncommitted&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;발생할 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: center;&quot;&gt;발생할 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: center;&quot;&gt;발생할 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Read Committed&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: center;&quot;&gt;발생할 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: center;&quot;&gt;발생할 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Repeatable Read&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: center;&quot;&gt;발생할 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Serializable&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. 사실 다른 문제들도 있습니다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;널리 알려진 문제가 Read Uncommitted, Non-Repeatable Read, Phantom Read일 뿐 DB에서 발생 가능한 동시성 관련 문제들은 더 많습니다. 대표적으로 Lost Update, Write Skew 등이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) Lost Update&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;두 트랜잭션이 동시에 같은 데이터를 업데이트하는 경우, 한 트랜잭션의 변경분이 다른 트랜잭션에 의해&lt;/span&gt; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;덮어쓰여지는 것&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;을 말합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가령 x에 10이 있을 때&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;font-family: 'Noto Sans Light';&quot;&gt;Tx A : X에 20을 더함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx B : X에 10을 더함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;을 한다고 가정해보겠습니다. x에는 총 30이 더해질 것으로 예상되나, Tx A가 진행되는 사이 Tx B가 실행되면 다음 문제가 발생 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;934&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUGte4/btsLvybGVRE/ja24dSaoUkIxY4NfwazD20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUGte4/btsLvybGVRE/ja24dSaoUkIxY4NfwazD20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUGte4/btsLvybGVRE/ja24dSaoUkIxY4NfwazD20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUGte4%2FbtsLvybGVRE%2Fja24dSaoUkIxY4NfwazD20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;723&quot; height=&quot;510&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx B가 실행되어 x에 20이 저장됐으나, Tx A는 본인이 원래 읽었던 10에 20을 더한 30을 write하게 되어 결론적으로 x값은 30이 아닌 20만 증가됐습니다. 즉 Tx B가 수행한 변경분이 Tx A에 의해 덮어씌워진 셈이며, 이는 일관되지 않은 결과를 초래할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) Write Skew&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;두 트랜잭션이 서로 독립적으로 데이터를 읽고 검증한 후 업데이트할 때&lt;/span&gt; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 무결성이 깨지는 것&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;을 말합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가령 x, y가 각각 1이고 x + y &amp;gt;= 1이어야 한다는 제약이 있다고 할 때&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx A : x에서 1을 뺌&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx B : y에서 1을 뺌&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;을 한다고 가정해보겠습니다. 현실 세계에선 현재 병원에 의사가 두 명 재직 중인데, 뭐가 됐든 의사 한 명은 재직 중이어야 하는 제약이 있는 상황에서 두 의사가 동시에 휴직 신청을 한 경우로 빗댈 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;944&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zpHEx/btsLuLbyquS/SblBSthraLLz4FjofQF5zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zpHEx/btsLuLbyquS/SblBSthraLLz4FjofQF5zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zpHEx/btsLuLbyquS/SblBSthraLLz4FjofQF5zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzpHEx%2FbtsLuLbyquS%2FSblBSthraLLz4FjofQF5zK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;697&quot; height=&quot;552&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Tx A에서 먼저 x를 0으로 업데이트했습니다. 만약 트랜잭션이 직렬로 수행됐다면 Tx B는 실행되지 않았어야 했을 것입니다. 그러나 Tx B가 데이터를 읽은 시점에서는 x, y 모두 1이었으니 Tx B도 y를 0으로 업데이트해줬습니다. 결국 각 트랜잭션이 서로 다른 데이터를 업데이트한 것이지만 결과적으론 x, y가 모두 0이 되어 데이터 무결성이 깨지게 된 셈입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 문제들 외에도 여러 문제가 존재합니다. 물론 어떤 문제들은 적절히 격리 수준을 설정하는 것으로 막아줄 수 있고, DB 엔진들은 저마다 각 격리 수준을 구현하는 방법이 다르기 때문에 생각보다 더 넓은 범위의 문제들을 방지해주기도 합니다. 예를 들어 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;MySQL InnoDB는 Repeatable Read 수준을 사용해도 Next-Key Lock 등을 통해 Phantom Read 문제를 방지해주고, PostgreSQL은 Snaption Isolation이란 형태로 Repeatable Read를 구현하여 Phantom Read 문제를 방지해줍니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 DB에서 제공하는 격리 수준으로 방지할 수 없는 문제들도 있습니다. 그리고 격리 수준은 데이터를 읽기에 관련된 문제들을 방지해주는 것으로 쓰기 작업같은 경우 처리된 결과가 논리적으로 무결한 상태임을 보장하진 않습니다. Write Skew같은 문제는 Repeatable Read같은 격리 수준에서도 발생 가능한 것을 예시로 들 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;즉 개발자는 시스템의 요구사항에 맞는 적절한 격리 수준을 선택하는 것도 중요하나, 다른 전략을 추가적으로 활용해서 DB에서 발생되는 동시성 문제들을 해결할 수 있어야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&amp;nbsp;이를 위해 낙관적 동시성 제어, 비관적 동시성 제어로 대표되는 동시성 제어 전략을 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;TMI : 두 동시성 제어 전략은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;낙관적 락, 비관적 락&lt;/b&gt;&lt;/span&gt;이라는 이름으로 더 많이 알려져 있는 것으로 보입니다. 그러나 둘 다 락을 사용하는 전략은 아니기 때문에 동시성 제어가 더 정확한 표현이라고 생각되어 이 글을 쓰면서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;동시성 제어&lt;/b&gt;&lt;/span&gt;라는 이름으로 작성했습니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6. 동시성 제어 전략 : 낙관적 동시성 제어와 비관적 동시성 제어&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이름에서 알 수 있듯이 낙관적 동시성 제어는 &quot;충돌이 나지 않을 거야&quot;라고 가정하여 최대한 동시성을 확보한 뒤 커밋 직전에 충돌 여부를 확인하는 방법이고, 비관적 동시성 제어는 &quot;충돌이 분명 날 거야&quot;라고 가정하여 처음부터 충돌 자체를 방지하는 메커니즘입니다. 자세히 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 낙관적 동시성 제어 (Optimistic Concurrency Control)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;자원에 락을 걸지 않고 작업을 수행하다가 커밋 시점에 다른 트랜잭션에 의해 데이터가 변경됐는지 확인하는 전략&lt;/b&gt;&lt;/span&gt;으로, 충돌이 나지 않을 것이라 가정하고 자원에 대한 잠금 없이 높은 동시성을 유지하면서 작업을 처리하는 개념입니다. 보통 데이터를 읽을 때 그 시점의 상태 정보(버전 정보 등)을 저장해뒀다가 데이터 변경 시점에 상태 정보를 비교하여 변화가 있다면 다른 트랜잭션이 데이터를 변경한 것으로 간주하고 갱신을 취소하거나 롤백하는 형태로 구현됩니다. 만약 상태 정보가 같다면 상태 정보 갱신도 함께 수행됩니다. 락을 사용하지 않아 높은 동시성을 제공할 수 있으므로 데이터 충돌이 적은 환경에서 사용한다면 성능 최적화를 기대할 수 있다는 장점이 있으나, 충돌이 자주 발생하는 환경이라면 갱신 실패 또는 롤백이 반복되며 성능 저하의 주범이 될 수 있다는 단점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 비관적 동시성 제어 (Pessimistic Concurrency Control)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;자원(테이블, 레코드 등)에 락을 걸고 작업을 수행하여 다른 트랜잭션의 접근을 차단하는 전략&lt;/b&gt;&lt;/span&gt;으로, 충돌이 발생함을 가정하고 리소스에 대한 락을 미리 획득하여 특정 기간 동안 해당 리소스에 대한 독점적인 액세스를 유지하는 개념입니다. 보통 SELECT FOR UPDATE를 사용해 데이터를 읽는 시점부터 배타 락을 설정하는 형태 등으로 구현됩니다. 충돌 가능성을 사전에 차단하는 만큼 데이터 무결성을 강력히 보장하고 충돌이 자주 발생하는 상황에선 낙관적 락보다 좀 더 나은 성능을 기대할 수 있다는 장점이 있으나, 락으로 인한 동시성 저하 및 락 획득을 위한 대기 시간 증가, 데드락 발생 가능성이 존재한다는 단점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;공유 락(Shared Lock) : Read Lock으로도 불리며 자원에 대한 쓰기는 안 되지만 자원을 읽는 동안 다른 트랜잭션에서 데이터를 읽는 것은 허용하는 개념의 락입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;배타 락(Exclusive Lock) : Write Lock으로도 불리며 자원에 대해 다른 트랜잭션의 읽기와 쓰기를 모두 막는 개념의 락입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;참고&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;a href=&quot;https://cybernerdie.medium.com/database-transactions-explained-a-deep-dive-into-reliability-17ab4e17117a&quot;&gt;https://cybernerdie.medium.com/database-transactions-explained-a-deep-dive-into-reliability-17ab4e17117a&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start; font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/407507&quot;&gt;https://d2.naver.com/helloworld/407507&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=sLJ8ypeHGlM&amp;amp;t=804s&quot;&gt;https://www.youtube.com/watch?v=sLJ8ypeHGlM&amp;amp;t=804s&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=bLLarZTrebU&amp;amp;list=LL&amp;amp;index=9&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=bLLarZTrebU&amp;amp;list=LL&amp;amp;index=9&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://medium.com/@iamssrofficial/concurrency-in-databases-database-isolation-levels-dirty-read-phantom-read-non-repeatable-read-320ff3553d6d&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@iamssrofficial/concurrency-in-databases-database-isolation-levels-dirty-read-phantom-read-non-repeatable-read-320ff3553d6d&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://medium.com/@abhirup.acharya009/managing-concurrent-access-optimistic-locking-vs-pessimistic-locking-0f6a64294db7&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@abhirup.acharya009/managing-concurrent-access-optimistic-locking-vs-pessimistic-locking-0f6a64294db7&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB</category>
      <category>Acid</category>
      <category>ISOLATION LEVEL</category>
      <category>Transaction</category>
      <category>격리 수준</category>
      <category>낙관적 락</category>
      <category>비관적 락</category>
      <category>트랜잭션</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/145</guid>
      <comments>https://jofestudio.tistory.com/145#entry145comment</comments>
      <pubDate>Sun, 29 Dec 2024 01:34:06 +0900</pubDate>
    </item>
    <item>
      <title>분산 메시지 큐 설계</title>
      <link>https://jofestudio.tistory.com/144</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;※&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: justify;&quot;&gt;&amp;nbsp;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권 - 챕터 4의 내용과 개인적으로 추가 공부한 내용들을 정리한 글입니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #333333; text-align: justify;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현대적인 소프트웨어 아키텍쳐들은 각자만의 인터페이스를 갖고 분리된 여러 컴포넌트들로 구성됩니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;메시지큐는 이 컴포넌트 사이에서의 통신을 담당하는 컴포넌트로, 각 컴포넌트들이 기존에 sync 방식으로 통신하던 것을 async 방식으로 쉽게 통신할 수 있도록 돕는 역할&lt;/b&gt;&lt;/span&gt;을 합니다. 메시지 큐 사용 시 다음과 같은 이점들이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결합도 완화 : 각 컴포넌트 사이에 통신을 담당하는 컴포넌트를 두는 것이므로, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 컴포넌트 간의 결합도가 낮아지게 됩니다&lt;/b&gt;&lt;/span&gt;. 이를 통해 좀 더 유연한 설계 및 각 컴포넌트의 독립적인 갱신이 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;확장성 증가 : 메시지 큐를 사용하는 컴포넌트는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;데이터를 생산하는 생산자(producer)와 소비자(consumer)로 분류 가능&lt;/b&gt;&lt;/span&gt;합니다. 결합도가 완화된 것을 통해, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;시스템 부하에 맞춰 각 컴포넌트의 규모를 독립적으로 늘릴 수 있습니다&lt;/b&gt;&lt;/span&gt;. (트래픽이 더 몰려 더 많은 데이터가 생산되면 소비자를 증설 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가용성 개선 : 특정 컴포넌트에 장애가 생겨도 다른 컴포넌트는 메시지 큐와 상호작용 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;성능 개선 : 메시지 큐는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;각 컴포넌트들이 async 통신을 쉽게 할 수 있도록 합니다&lt;/b&gt;&lt;/span&gt;. 생산자는 소비자가 어떻게 처리하든 메시지큐에 데이터를 밀어넣어주면 되고, 소비자는 메시지큐에 있는 데이터를 긁어가서 처리하면 됩니다. 이를 통해 sync 방식에서 생기던 문제점들(ex : 자원을 비효율적으로 사용 등)을 개선할 수 있습니다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 장점들만 있는 건 아닙니다. 메시지 큐라는 컴포넌트를 추가 운영하면서 생기는 공수도 있을 것이고, 컴포넌트 간의 통신이 메시지 큐라는 컴포넌트를 거쳐 발생하므로 네트워크 문제로 인한 추가 지연이 발생 가능한 것, 전반적인 구현 복잡성 증가 등의 단점들도 있습니다. 그럼에도 메시지큐는 기존 컴포넌트들간의 결합도를 낮추면서 얻을 수 있는 이점들이 많기 때문에 많은 시스템에서 사용하는 컴포넌트이기도 합니다(해외에서는 메시지큐를 이미 성숙된 기술이라고도 표현합니다).&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지큐는 대표적으로 RabbitMQ등의 서비스가 있으며 Apache Kafka 등의 이벤트 브로커도 많이 쓰입니다. 이 글에서는 분산 메시지 큐를 설계하는 방법과 필요한 지식 등을 소개하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;※ 참고 : 메시지 큐 vs 메시지 브로커 vs 이벤트 브로커&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 메시지 큐&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;컴포넌트 간의 메시지를 전달하기 위한 컴포넌트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지를 저장하는 역할&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 메시지 브로커&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지 큐의 기능을 포함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;추가적으로 메시지 라우팅(pub/sub 패턴 등을 통해..), 메시지 변환 등도 해줌&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) 이벤트 브로커&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a id=&quot;user-content-3-이벤트-브로커&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/monthly-cs/2024-11-system-design-interview-2/blob/main/%EC%A1%B0%EC%83%81%ED%98%84_Cho_SangHyun/2024_11_24_dil.md#3-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B8%8C%EB%A1%9C%EC%BB%A4&quot;&gt;&lt;/a&gt;&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지 브로커의 기능을 포함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;추가적으로 메시지(이벤트) 보관 등의 기능을 할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;※ 설계 범위&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 설계에서 메시지는 텍스트만 가정하며, KB수준으로 가정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 기능적 요구사항&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;생산자는 메시지 큐에 메시지를 보낼 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소비자는 메시지 큐에서 메시지를 소비할 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지는 생산된 순서대로 소비자에게 전달되어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지 큐에 넣어진 메시지는 설정에 따라 소비자가 반복적으로 소비할 수도 있고, 한 번만 소비할 수도 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지는 2주까지만 보관합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지 전달은 최소 한 번, 최대 한 번, 정확히 한 번 중에 설정 가능해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 비기능적 요구사항&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;높은 대역폭 제공 / 낮은 전송 지연 중 하나를 설정할 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지 양이 급증해도 처리할 수 있도록 확장성있는 설계가 필요합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터는 디스크에 지속적으로 보관해야 하며 여러 노드에 복제되어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고) 아래에서 좀 더 설명하겠지만, 높은 대역폭을 제공하려면 메시지를 버퍼링하다가 일괄로 처리해야 하나 이는 메시지들이 즉시 전달되는게 아니므로 전송 지연이 높아지게 됩니다. 즉 높은 대역폭과 낮은 전송 지연을 만족시키는 것은 힘듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;메시지큐의 구성 요소와 개략적인 설계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지 큐의 핵심 구성 요소와 간략한 흐름도는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1787&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6LCea/btsKWk7PVB6/OfdZFFBPnWUFeqdNWuiIhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6LCea/btsKWk7PVB6/OfdZFFBPnWUFeqdNWuiIhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6LCea/btsKWk7PVB6/OfdZFFBPnWUFeqdNWuiIhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6LCea%2FbtsKWk7PVB6%2FOfdZFFBPnWUFeqdNWuiIhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1787&quot; height=&quot;227&quot; data-origin-width=&quot;1787&quot; data-origin-height=&quot;227&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;생산자가 메시지를 메시지큐에 발행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소비자는 큐를 구독하고 있고, 구독한 메시지를 소비하게 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 기반으로 한 메시지 모델들은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;메시지 모델&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 일대일 모델&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지를 소비하는 소비자가 여러 명일 수 있는데, 일대일 모델은 생산자가 메시지 큐에 발행한 메시지는 오직 한 소비자만 가져갈 수 있는 모델을 말합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1755&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mMsNK/btsKV2sSA3E/Kcky9tlmejkkCKNMUGfmk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mMsNK/btsKV2sSA3E/Kcky9tlmejkkCKNMUGfmk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mMsNK/btsKV2sSA3E/Kcky9tlmejkkCKNMUGfmk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmMsNK%2FbtsKV2sSA3E%2FKcky9tlmejkkCKNMUGfmk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1755&quot; height=&quot;694&quot; data-origin-width=&quot;1755&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 발행-구독 모델&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;관련있는 메시지들을 토픽이라는 논리적인 그룹으로 묶어 메시지들을 주제별로 관리하게 한 다음, 이 토픽으로 메시지를 보내고 받는 모델입니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;토픽에 전달된 메시지는 해당 토픽을 구독하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;모든 구독자들에게 전달&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;됩니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1755&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J0PRs/btsKZcOzkyK/ZEkt19Om7M9fJtDH2rrLk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J0PRs/btsKZcOzkyK/ZEkt19Om7M9fJtDH2rrLk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J0PRs/btsKZcOzkyK/ZEkt19Om7M9fJtDH2rrLk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ0PRs%2FbtsKZcOzkyK%2FZEkt19Om7M9fJtDH2rrLk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1755&quot; height=&quot;694&quot; data-origin-width=&quot;1755&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지 큐 컴포넌트의 구성 요소들은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;토픽, 파티션, 브로커&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;토픽 : &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;메시지 큐에서 데이터를 주고받는 주제 또는 채널&lt;/b&gt;&lt;/span&gt;의 개념으로, 관련있는 메시지들을 토픽으로 묶어 주제별로 관리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;파티션 : &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;토픽을 샤딩하여 만든 분할된 각 단위&lt;/b&gt;&lt;/span&gt;를 파티션이라 부릅니다. 토픽의 확장성과 병렬성을 위해 사용하며, 덕분에 토픽에 데이터가 몰려서 부하가 커지는 것을 방지할 수 있습니다. 파티션은 다수의 메시지 큐 컴포넌트들이 구성하는 클러스터에 고르게 분산 배치됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;브로커 : 파티션들을 유지하는, 즉 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;파티션들이 있는 서버&lt;/b&gt;&lt;/span&gt;를 브로커라고 부릅니다.&amp;nbsp;&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;1385&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEwERv/btsKY4cbg6X/XuYbuZP2znmCyRAVV4s2ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEwERv/btsKY4cbg6X/XuYbuZP2znmCyRAVV4s2ok/img.png&quot; data-alt=&quot;참고 : 파티션 내의 메시지 위치는 오프셋이라 부름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEwERv/btsKY4cbg6X/XuYbuZP2znmCyRAVV4s2ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEwERv%2FbtsKY4cbg6X%2FXuYbuZP2znmCyRAVV4s2ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1385&quot; height=&quot;412&quot; data-origin-width=&quot;1385&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참고 : 파티션 내의 메시지 위치는 오프셋이라 부름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;생산자가 보내는 메시지는 보내질 토픽의 파티션 중 하나로 보내지며, 메시지에 키 값을 설정하여 같은 키 값을 가진 메시지들은 같은 파티션으로만 보내지도록 설정할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;같은 토픽을 구독하는 소비자가 여러 명이면 각 구독자는 해당 토픽의 파티션들을 분담해서 처리하게 됩니다. 즉 이 경우 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;모든 소비자들에 구독한 토픽에 발행되는 모든 메시지를 처리하는 게 아니라 서로가 분담해서 처리하는 것&lt;/b&gt;&lt;/span&gt;으로 이해할 수 있습니다. 이 소비자들을&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;소비자 그룹&lt;/b&gt;&lt;/span&gt;이라고 부릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;소비자 그룹&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소비자 그룹의 소비자들은 앞서 설명했듯 특정 토픽의 메시지들을 서로 분담해서 처리합니다(즉 서로 협력하는 구조). &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;같은 토픽에 여러 소비자 그룹이 있을 수 있고, 하나의 소비자 그룹이 여러 토픽을 처리하고 있을 수도 있습니다&lt;/b&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ex 1) 주문 토픽을 구독하면서 결제를 담당하는 그룹과 재고 차감을 담당하는 그룹이 있을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ex 2) 사용자 활동 토픽, 주문 토픽를 둘 다 구독하고 있는 분석용 그룹이 있을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 다음 그림과 같이 특정 소비자 그룹 내의 소비자들이 같은 파티션에 붙으면 어떻게 될지도 생각해볼 필요가 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1205&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btUl1o/btsKYiWeWYm/Do4vedtK3tRLaXAkDZ38z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btUl1o/btsKYiWeWYm/Do4vedtK3tRLaXAkDZ38z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btUl1o/btsKYiWeWYm/Do4vedtK3tRLaXAkDZ38z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtUl1o%2FbtsKYiWeWYm%2FDo4vedtK3tRLaXAkDZ38z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;794&quot; height=&quot;278&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 경우, 같은 파티션에 있는 메시지들을 두 개 이상의 소비자가 병렬로 읽게 되므로 대역폭 측면에선 좋겠으나 메시지를 순서대로 소비하는 것이 보장되지 않습니다(병렬처리이므로 순서보장이 어려움). 이 문제는 직관적으로 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;한 파티션은 한 소비자 그룹에선 한 소비자만 담당하게 하기&quot;&lt;/b&gt;&lt;/span&gt;로 해소가 가능합니다. 다만 파티션 수 &amp;lt; 소비자 수 일 때 일부 소비자들은 유휴 현상이 발생 가능하다는 단점이 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;개략적 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;1770&quot; data-origin-height=&quot;1021&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b623aW/btsKZ0NnqEB/zMsstrZ0X48HboRKUvGVL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b623aW/btsKZ0NnqEB/zMsstrZ0X48HboRKUvGVL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b623aW/btsKZ0NnqEB/zMsstrZ0X48HboRKUvGVL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb623aW%2FbtsKZ0NnqEB%2FzMsstrZ0X48HboRKUvGVL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1770&quot; height=&quot;1021&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;1021&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;생산자 : 메시지를 특정 토픽으로 보내는 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소비자(소비자 그룹) : 토픽을 구독하고 발행된 메시지를 소비하는 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;브로커 : 토픽을 샤딩(즉 분할)한 단위인 파티션들이 있는 서버&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;상태 저장소 : 각 브로커별로 갖고 있는 파티션의 상태 정보들(각 소비자 그룹이 마지막으로 가져간 메시지의 오프셋 등)을 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메타데이터 저장소 : 토픽 설정과 속성 정보들(토픽별 파티션 수, 메시지 보관 기간 등).. 즉 브로커들이 공통적으로 참조하는 메타데이터들을 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;조정 서비스(Coordination Service) : 브로커들의 상태 모니터링, &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;리더 선출(분산 시스템들이 master - slave 구조일 때 master가 다운되면 누가 master가 될지를 선출하는 것) 등을 담당&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;상세 설계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;파티션(데이터 저장소) 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 파티션에서의 메시지 저장 방식&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메시지들은 토픽에 저장되며, 토픽은 파티션이라는 단위로 샤딩된다고 말씀드렸습니다. 이 파티션에 데이터를 어떻게 저장해볼지를 생각해봐야 하는데요, 대표적으로 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DB에 저장하는 것과 파일 형태로 디스크에 저장하는 방식&lt;/b&gt;&lt;/span&gt;을 생각해볼 수 있습니다. 분산 메시지 큐는 시스템 특성 상 메시지라는 데이터를 write(생산)하고 read(소비)하는 연산이 빈번하게 발생하므로 DB에 저장해 관리하는 것은 한계가 있습니다(대규모의 읽기와 쓰기가 모두 능한 DB는 설계가 어려움). 근데 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;메시지를 생산하고 소비하는 과정을 보면 읽기/쓰기 모두 메시지에 대한 액세스 패턴이 순차적&lt;/b&gt;&lt;/span&gt;이므로, 로그파일 형태로 디스크에 저장하도록 설계하면 좋은 효과를 기대할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;616&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnhlkS/btsKZu2NUeA/vGx73t6fkrAojnDTmR5cKK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnhlkS/btsKZu2NUeA/vGx73t6fkrAojnDTmR5cKK/img.jpg&quot; data-alt=&quot;하드 디스크는 플래터가 회전하며 읽기/쓰기가 이루어지므로, 데이터 액세스 패턴이 순차적일 때 효과적&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnhlkS/btsKZu2NUeA/vGx73t6fkrAojnDTmR5cKK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnhlkS%2FbtsKZu2NUeA%2FvGx73t6fkrAojnDTmR5cKK%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;559&quot; height=&quot;410&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하드 디스크는 플래터가 회전하며 읽기/쓰기가 이루어지므로, 데이터 액세스 패턴이 순차적일 때 효과적&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 파티션 복제 (파티션 사본)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;파티션들을 복제하여 높은 가용성을 제공&lt;/b&gt;&lt;/span&gt;하도록 할 수 있습니다. 토픽을 여러 파티션으로 분할한 뒤, 각 브로커들이 다음과 같이 파티션들의 사본도 갖도록 구성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1297&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddtUq1/btsK061cJs1/6a5BiFx35nIIE2wGhCG0V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddtUq1/btsK061cJs1/6a5BiFx35nIIE2wGhCG0V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddtUq1/btsK061cJs1/6a5BiFx35nIIE2wGhCG0V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddtUq1%2FbtsK061cJs1%2F6a5BiFx35nIIE2wGhCG0V0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;681&quot; height=&quot;286&quot; data-origin-width=&quot;1297&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DB도 master에만 write를 하면 사본들로 복제가 되는 것처럼, 생산자가 메시지를 발행할 때도 1번 파티션에 발행해야 한다면 리더역할을 하는 파티션에 발행하도록 한 뒤&amp;nbsp; 사본 파티션들은 리더에서 지속적으로 메시지들을 가져와 동기화하도록 구성할 수 있습니다. 이때 파티션 별로 사본을 어떻게 분산할 지에 대해 기술하는 것을 사본 분산 계획(replica distribution plan)이라 부르며 다음과 같이 요약할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;주문 토픽의 1번 파티션 사본 분산 계획 : 파티션 총 2개. 리더는 1번 브로커, 사본은 2번 브로커에 배치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주문 토픽의 2번 파티션 사본 분산 계획 : 파티션 총 2개. 리더는 2번 브로커, 사본은 1번 브로커에 배치&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;파티션의 리더는 조정 서비스를 통해 브로커 노드 중 하나가 선출되도록 합니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 브로커가 사본 분산 계획을 만들고 메타데이터 저장소로 저장하도록 합니다. (조정 서비스에 대한 내용은 밑에서 소개하겠습니다).&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) 파티션 사본 동기화&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DB에서 복제본을 구성할 때, 동기식 / 비동기식 / 반동기식으로 복제 방법을 구성할 수 있습니다. 동기식은 master에 데이터가 쓰인 후 복제본까지 데이터가 쓰인 후에 ack를 보내는 거고, 비동기는 master노드에만 데이터를 쓴 뒤 ack를 보내는 거고, 반동기는 master에 데이터를 쓴 후 복제본으로 데이터 변경에 대한 로그 작성이 끝난 것만 확인한 뒤 ack를 보내는 것을 의미합니다. 앞서 설명한 것처럼 파티션의 동기화도 리더 파티션에 발행된 메시지를 사본 파티션들이 가져가게끔 구성하는데, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;메시지 발행에 대한 ack를 어떻게 보내는지 설정해주는 것을 통해 영속성의 정도를 조절&lt;/b&gt;&lt;/span&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 때 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ISR(In-Sync Replicas)이란 개념이 사용&lt;/b&gt;&lt;/span&gt;됩니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;리더 파티션과 동기화된 사본 파티션&lt;/b&gt;&lt;/span&gt;을 일컫는 말이며, 이때 &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;동기화됐다&quot;의 기준은 토픽 설정에 따라 달라집니다&lt;/b&gt;&lt;/span&gt;. 리더 파티션의 메시지 개수와 사본 파티션의 메시지 개수는 차이가 날 수도 있는데, 설정된 특정 개수 이하로 차이난다면 &quot;동기화됐다&quot;라고 취급할 수 있습니다. 만약 메시지 차이가 2개 이하면 ISR로 취급한다고 할 경우, 다음 상황에선 사본 1과 2가 ISR입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1527&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbkrYX/btsKZdubzn3/XkzwCGVEeoWqZPqq69LhK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbkrYX/btsKZdubzn3/XkzwCGVEeoWqZPqq69LhK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbkrYX/btsKZdubzn3/XkzwCGVEeoWqZPqq69LhK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbkrYX%2FbtsKZdubzn3%2FXkzwCGVEeoWqZPqq69LhK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;762&quot; height=&quot;443&quot; data-origin-width=&quot;1527&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;리더 파티션에 새로운 메시지가 발행된 후, ISR 상태인 파티션들에게까지 메시지가 전달된 뒤 ack를 보내는 것을 ACK=all이라 합니다. 리더 파티션에 메시지가 발행된 후(즉 메시지가 저장된 후) ack를 보내면 ACK=1, 리더 파티션에 메시지를 전달한 후 ack를 보내는 것을 ACK=0(즉 메시지가 저장됐는지는 관심사가 아님)이라고 합니다. 설정이 높을수록 영속성을 높게 가져가는 것이고, 설정이 낮을수록 낮은 지연을 기대할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;생산자, 소비자 작업 흐름 설계 및 일괄 처리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;보통 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;반복되는 작업을 일괄 처리(batching)하면 네트워크 I/O 또는 디스크 I/O를 줄이게 되며 성능을 개선시킬 수 있습니다&lt;/b&gt;&lt;/span&gt;(다만 데이터들을 모았다가 한 번에 처리하는 것이므로 개별 데이터의 지연은 높아질 수 있음). 분산 메시지 큐에서는 다음과 같이 생산자와 소비자의 작업 흐름을 구성하며 일괄 처리 도입도 설계해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 생산자에서의 작업 흐름 및 일괄 처리 설계&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 일괄 처리를 배제하고 생각해보겠습니다. 생산자가 새 메시지를 발행할 경우 리더 파티션이 있는 브로커로 메시지를 보내야 합니다. 이때 해당 파티션을 유지하는 브로커로 연결하기 위해 별도의 라우팅용 컴포넌트를 두는 방안을 고려할 수 있으나, 다음과 같이 라우팅 계층을 생산자 내부로 편입시키는 구조를 설계해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1297&quot; data-origin-height=&quot;831&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5U9yT/btsK1yJ07SF/2vEtjKtDQhgKKMDETI7vk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5U9yT/btsK1yJ07SF/2vEtjKtDQhgKKMDETI7vk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5U9yT/btsK1yJ07SF/2vEtjKtDQhgKKMDETI7vk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5U9yT%2FbtsK1yJ07SF%2F2vEtjKtDQhgKKMDETI7vk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;399&quot; data-origin-width=&quot;1297&quot; data-origin-height=&quot;831&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라우팅 컴포넌트를 별도로 두면 그에 따른 네트워크 지연을 감수해야 하지만, 이렇게하면 거치는 컴포넌트가 하나 더 적으니 전송 지연이 줄어드는 효과를 줍니다. 또한 생산자 입장에서는 전송할 메시지를 버퍼에 모으다 목적지로 일괄 전송하게끔 처리하여 대역폭을 높일 수 있다는 장점을 가져올 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 소비자에서의 작업 흐름 및 일괄 처리 설계&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소비자의 작업 흐름 설계 시, 데이터를 Push / Pull 중 어떤 방식으로 가져올 것인지를 고려해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;Push : 브로커에서 소비자로 직접 데이터를 밀어주는 방식. 메시지가&amp;nbsp;발행되자마자&amp;nbsp;소비자에게&amp;nbsp;밀어줄&amp;nbsp;수&amp;nbsp;있으니&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;지연이&amp;nbsp;낮다는&amp;nbsp;장점&lt;/b&gt;&lt;/span&gt;이&amp;nbsp;있으나,&amp;nbsp;데이터&amp;nbsp;공급의&amp;nbsp;주도권이&amp;nbsp;브로커에게&amp;nbsp;있는&amp;nbsp;만큼&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;소비자가&amp;nbsp;감당&amp;nbsp;가능한&amp;nbsp;양&amp;nbsp;이상으로&amp;nbsp;데이터를&amp;nbsp;넣어줄&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;단점&lt;/b&gt;&lt;/span&gt;이&amp;nbsp;있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Pull : 소비자가 브로커에서 직접 데이터를 가져오는 방식. 데이터&amp;nbsp;공급의&amp;nbsp;주도권이&amp;nbsp;소비자에게&amp;nbsp;있으니&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;자신의&amp;nbsp;속도에&amp;nbsp;맞게&amp;nbsp;브로커로부터&amp;nbsp;데이터를&amp;nbsp;가져올&amp;nbsp;수&amp;nbsp;있고,&amp;nbsp;일괄&amp;nbsp;처리에&amp;nbsp;적합하다는&amp;nbsp;장점&lt;/b&gt;&lt;/span&gt;이&amp;nbsp;있습니다.&amp;nbsp;반면&amp;nbsp;브로커에&amp;nbsp;메시지가&amp;nbsp;없어도&amp;nbsp;소비자가&amp;nbsp;데이터를&amp;nbsp;가져가려&amp;nbsp;시도할&amp;nbsp;수&amp;nbsp;있으며&amp;nbsp;이는&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;컴퓨팅&amp;nbsp;자원의&amp;nbsp;낭비(하지&amp;nbsp;않아도&amp;nbsp;되는데&amp;nbsp;하는&amp;nbsp;것이므로)가&amp;nbsp;된다는&amp;nbsp;단점&lt;/b&gt;&lt;/span&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 레디스 pub / sub은 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Push&lt;/span&gt; 방식을, kafka는 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Pull&lt;/span&gt; 방식을 사용합니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Pull&lt;/span&gt; 방식 사용 시 컴퓨팅 자원이 낭비될 수 있는 부분은 롱 폴링(서버에 요청을 보내고 서버가 새로운 데이터가 있을 때까지 응답을 지연시키는 방식)을 통해 어느 정도 해소가 가능해 대부분의 메시지 큐는 Pull 방식을 많이 지원합니다. 해당 방식의 작업 흐름을 도식화하면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1669&quot; data-origin-height=&quot;813&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZnWcU/btsK03w5gqA/MI2P4ISI6oIVqEpSclo2wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZnWcU/btsK03w5gqA/MI2P4ISI6oIVqEpSclo2wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZnWcU/btsK03w5gqA/MI2P4ISI6oIVqEpSclo2wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZnWcU%2FbtsK03w5gqA%2FMI2P4ISI6oIVqEpSclo2wK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;352&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;813&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소비자 입장에서는 내가 어느 파티션에 붙어서 데이터를 읽고 써야 할지를 알아야 하는데, 이걸 알려주는 역할을 해당 소비자 그룹의&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;코디네이터 브로커&lt;/b&gt;&lt;/span&gt;가 해주며 소비자 그룹 이름의 해시값에 매핑되는 브로커가 담당합니다. 즉 서로 다른 소비자 그룹에 대해 같은 브로커가 두 그룹의 코디네이터 역할을 할 수도 아닐 수도 있습니다. 코디네이터 브로커를 통해 어떤 파티션에 붙어야 하는지를 알게 됐다면, 해당 파티션의 오프셋으로부터 메시지를 묶어서 가져오는 식으로 일괄 처리를 설계할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 소비자 그룹의 코디네이터 브로커는 신규 소비자에 대한 파티션 지정 외에도 그룹 내 소비자들의 상태 감시(heart beat를 통해), 소비자 탈퇴 및 장애 발생 시 파티션 재분배 등의 역할도 담당합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;상태 저장소 &amp;amp; 메타데이터 저장소 &amp;amp; 조정 서비스 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;브로커가 유지하는 파티션에 대한 상태 정보를 관리하는 저장소와 토픽의 메타데이터를 관리하는 저장소, 그리고 조정 서비스를 어떻게 설계할 지도 살펴봐야 합니다. 이때 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Apache Zookeeper&lt;/b&gt;&lt;/span&gt;라는 서비스를 사용해볼 수 있습니다. 분산 메시지 큐를 비롯한 여러 분산 시스템에선 각 시스템들의 상태를 모니터링할 수 있는 환경, 범용적으로 사용 가능한 중앙 집중식 데이터 저장소가 등이 필요합니다. 물론 이를 직접 구현할 수 있겠으나, Apache에서 분산 시스템들이 공통적으로 가지는 요구사항들을 해결하기 위해 Zookeeper라는 서비스를 개발했다고 이해할 수 있습니다. 대표적으로 다음 기능들을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시스템들이 공유하는 공유 상태의 저장 &amp;amp; 관리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 시스템 상태 모니터링 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;리더 선출 (분산 시스템들이 master - slave 구조일 때 leader가 다운되면 누가 leader가 될지를 선출하는 거. 이거 위해서 주키퍼는 노드 수를 홀수로 맞춘다고 합니다)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 통해 상태 저장소, 메타데이터 저장소, 조정 서비스의 역할을 Zookeeper가 담당하면서 다음 형태의 설계가 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1774&quot; data-origin-height=&quot;953&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kVPYI/btsK20G3JiU/eoWS8UKw8ZiUuMYAEnVCXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kVPYI/btsK20G3JiU/eoWS8UKw8ZiUuMYAEnVCXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kVPYI/btsK20G3JiU/eoWS8UKw8ZiUuMYAEnVCXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkVPYI%2FbtsK20G3JiU%2FeoWS8UKw8ZiUuMYAEnVCXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;341&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;953&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS</category>
      <category>pub sub</category>
      <category>분산 메시지 큐</category>
      <category>브로커</category>
      <category>비동기</category>
      <category>생산자</category>
      <category>설계</category>
      <category>소비자</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/144</guid>
      <comments>https://jofestudio.tistory.com/144#entry144comment</comments>
      <pubDate>Sat, 30 Nov 2024 14:37:13 +0900</pubDate>
    </item>
    <item>
      <title>위치 기반 서비스를 위한 아키텍처 설계 - 구글 맵</title>
      <link>https://jofestudio.tistory.com/143</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;※&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #333333; text-align: justify;&quot;&gt; 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권 - 챕터 3을 읽고 정리한 글입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위치 기반 서비스 설계에 대한 마지막 장으로, 구글 맵처럼 지도 탐색이나 경로 안내 등이 가능한 웹 기반의 지도 서비스를 설계해봅니다. 지도 이미지를 어떤 식으로 보여주도록 설계할 수 있는지, 경로 안내를 어떤 식으로 보여주도록 설계할 수 있는지 등에 대해 살펴보겠습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음과 같은 기능적 요구사항을 가정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주기적으로 사용자의 현재 위치를 갱신시킬 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;출발지와 목적지가 주어졌을 때 경로 안내를 할 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지도 표시를 할 수 있어야 하며, 사업장들의 위치나 사진은 표시하지 않아도 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;다음과 같은 비기능적 요구사항도 가정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자에게 잘못된 경로를 안내하면 안 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트는 가능한 한 최소한의 데이터와 배터리를 사용해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일반적으로 널리 통용되는 가용성과 확정성을 갖춰야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;개략적인 규모 추정&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;다음과 같이 가정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일간 활성 사용자(DAU) : &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;10억 명&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일별 경로 안내 기능 사용 시간 : &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;50억 분 &lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(각 사용자가 주당 35분 이 기능 사용)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;QPS : &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;20만&lt;/b&gt;&lt;/span&gt;으로 가정 (경로 안내 기능 사용 중에 사용자 위치 갱신)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;최대 QPS :평균치의 5배 = &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;100만&lt;/b&gt;&lt;/span&gt;으로 가정&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그리고 기본적으로 다음 4가지의 데이터를 보유한 상태로 시작해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;세계 지도 이미지 : &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;하나의 커다란 이미지를 표시하는 대신 작은 타일들로 쪼갠 뒤 클라이언트는 필요한 영역의 타일들만 이어붙여 보도록 할 수 있습니다. 확대 수준에 따라 이미지 세트를 여러 개 준비해야 하며, 여기서는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;총 100PB&lt;/b&gt;&lt;/span&gt;가 소요된다고 가정합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메타데이터 : 각 지도 이미지들의 메타데이터로, 크기가 아주 작으므로 신경쓰지 않아도 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;도로 데이터 : 경로 안내에 사용되며 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;수 TB&lt;/b&gt;&lt;/span&gt; 수준의 raw data를 미리 확보해 둔 상태로 가정합니다. 경로 안내를 위해 그래프 형태의 자료구조로 변환할 필요가 있으나 변환 결과의 용량도 비슷하다고 유추할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위도 경도 페어 : 책에는 안 나와있으나.. 경로 안내를 위해 출발지 목적지를 위도 경도 페어로 바꿔야 하는데 이 데이터들을 지오코딩DB에 저장합니다. 하나의 레코드에 66Byte가 든다고 가정(주소 50, 위도 8, 경도 8)하고 전 세계의 주소를 20억 개(추정..)로 한다면 러프하게 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;132GB&lt;/b&gt;&lt;/span&gt; 정도의 용량이 필요합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;개략적인 설계안 제시&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 시스템은 위치 갱신 기능, 도로 데이터를 통한 경로 안내 기능, 세계 지도 이미지를 통한 지도 표시 기능을 제공해야 합니다. 개략적으로 다음과 같은 설계안을 제시할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1401&quot; data-origin-height=&quot;1264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/urhV9/btsKRoAXfdv/lseuUREz42MNhVpSgifchK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/urhV9/btsKRoAXfdv/lseuUREz42MNhVpSgifchK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/urhV9/btsKRoAXfdv/lseuUREz42MNhVpSgifchK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FurhV9%2FbtsKRoAXfdv%2FlseuUREz42MNhVpSgifchK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;679&quot; data-origin-width=&quot;1401&quot; data-origin-height=&quot;1264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 위치 서비스의 개략적 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자의 위치를 기록하는 역할을 담당합니다. 클라이언트가 t초 간격으로 자기 위치를 전송하게 할 수 있으며, 해당 데이터 스트림을 활용해 실시간 교통 상황 감지(정체되는 구간 분석 등), 사용자 행동 양태 분석 등에 활용 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇다고 사용자 위치가 바뀔 때마다 서버로 위치 기록 요청을 보내면 부하가 상당합니다(책에선 하루에 50억 분을 경로 안내에 사용한다고 말하며, 그 시간 동안 위치 기록이 이루어집니다). 이때 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;클라이언트가 위치 변경을 15초 정도 버퍼링하다가 일괄로 위치 기록을 요청하는 식으로 구현하면 부하를 크게 줄일 수 있습니다&lt;/b&gt;&lt;/span&gt;. 그러나 여전히 부하가 크고 특히 쓰기 연산이 많이 일어날 것이므로, 쓰기 성능이 탁월(메모리에 데이터를 모으다가 한 번에 디스크로 쓰는 방식을 통해 디스크 I/O가 적음)한 카산드라DB를 사용 가능합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 경로 안내 서비스의 개략적 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;출발지에서 목적지로 가는 빠른 경로를 찾아주는 역할을 합니다. 문자열 주소로 주어지는 목적지와 출발지를 각각 알맞은 위도 경도 페어로 바꾸고(이를 지오코딩이라 함), 이를 통해 목적지까지 가는 경로를 찾아 사용자에게 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지오코딩용 DB는 읽기 연산 위주의 DB이고, 각 주소에는 하나의 위도 경도 페어만 대응되므로 레디스 등의 인메모리 키-밸류 DB를 사용하는 것이 성능 차원에서 유리합니다. 개략적 규모 추정에서 살펴봤듯&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;132GB의 데이터&lt;/b&gt;&lt;/span&gt;를 담고 있어야 하므로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;샤딩&lt;/b&gt;&lt;/span&gt;을 통한 데이터 분배를 고려할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 설명했듯 도로 데이터는 수 TB를 raw data로 가지고 있다고 가정하고 있으므로 이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;그래프 형태의 자료구조로 변환하여 경로 안내에 사용&lt;/b&gt;&lt;/span&gt;해야 합니다. 그러나 전세계 도로망을 하나의 그래프로 표현하면 메모리 + 성능 차원에서 비효율적이므로 여러 그래프로 분리한 다음 필요한 그래프만 가져와서 연산하는 것이 효율적입니다. 이를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2차원 공간을 여러 격자로 나누고(지오해싱 등 사용 가능), 각 격자별로 그래프를 관리&lt;/b&gt;&lt;/span&gt;하게 할 수 있습니다. 또한 각 격자(타일)들이 도로로 연결된 다른 타일에 대한 참조를 가지게 하고, 효과적인 경로 안내를 위해 계층적으로 타일을 구성시킬 수 있습니다. 참고로 이런 그래프 데이터는 메모리에 인접 리스트 형태로 두는 것이 일반적이나 이 시스템에선 수 TB 수준으로 양이 상당하므로, S3같은 객체 저장소에 두고 캐싱을 활용하게 하는 것이 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) 지도 표시의 개략적 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개략적인 규모 추정에서 언급했듯이 지도 이미지는 총 100PB가 존재하므로, 이를 클라이언트가 다 가지게 한 뒤에 필요한 부분만 너희가 알아서 렌더링해서 보라고 하는 것은 실용적이지 않습니다. 클라이언트가 보고 있는 위치와 확대 수준에 따라 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;필요한 영역의 이미지만 가져오는 접근법이 실용적&lt;/b&gt;&lt;/span&gt;입니다. 이때 미리 지도 타일들을 만들어 저장해두고 필요한 영역의 타일 집합들을 요청(지오해싱 등을 활용)해서 해당 타일들을 받아오도록 할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;지도 이미지의 양이 상당하고 이 서비스는 글로벌 서비스를 가정(DAU가 10억)&lt;/b&gt;&lt;/span&gt;하므로, &lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CDN을 통해 지도 이미지를 서비스&lt;/span&gt;&lt;/b&gt;하도록 구성할 수 있습니다. CDN 사용은 사용자들이 자신과 가까운 곳으로부터 컨텐츠를 받아볼 수 있으므로 지연속도가 빨라진다는 장점 등이 있으며, 다음 순서대로 동작하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트가 지도 타일 요청을 CDN에 보냄&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 이미지가 CDN을 통해 서비스된 적 없다면 지정된 원본 서버에서 해당 이미지를 가져와서 캐싱하고 사용자에게 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;CDN을 통해 서비스된 적 있다면 바로 사용자에게 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 잠깐 말했듯이 지도 이미지 요청 시 지오해시 값을 계산하여 요청할 수 있는데, 이때 클라이언트가 직접 지오해시를 계산하는 방법을 사용한다면 여러 클라이언트(웹, 앱 등)를 따로 관리해야 하는 불편함이 생길 수 있습니다. 따라서 지오해시 값을 계산하는 서비스를&amp;nbsp; 별도로 분리하면 좀 더 유연성을 가져갈 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1218&quot; data-origin-height=&quot;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v5wkb/btsKQ5BDRpL/el0KkgUeDHsER9c6OrzQZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v5wkb/btsKQ5BDRpL/el0KkgUeDHsER9c6OrzQZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v5wkb/btsKQ5BDRpL/el0KkgUeDHsER9c6OrzQZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv5wkb%2FbtsKQ5BDRpL%2Fel0KkgUeDHsER9c6OrzQZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;406&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;상세 설계&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1) 위치 서비스의 상세 설계&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 살펴봤듯 쓰기 연산이 많이 일어나는 서비스이므로 카산드라같이 쓰기 성능에 좋은 DB를 고려할 수 있습니다. 카산드라는 키-밸류 데이터베이스이므로 (사용자 식별자, 위치 변경 타임스탬프)를 키로, 위도 경도 페어를 밸류로 기록하게끔 설계할 수 있습니다. 이때 사용자 식별자를 파티션 키(데이터를 어떤 노드에 저장할지 결정하는 데 사용)로 설정하고 타임스탬프를 클러스터링 키(동일한 파티션 내에서 데이터를 정렬하는 데 사용)로 설정한다면 특정 사용자의 특정 기간 내 위치를 효율적으로 읽도록 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자 위치를 DB에 기록하는 것과 별개로, 카프카같은 데이터 스트리밍 플랫폼을 통해 실시간 교통 상황 서비스 등의 개별 서비스로 위치 데이터 스트림을 전달하도록 설계할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1508&quot; data-origin-height=&quot;1185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9Figz/btsKRfYrYuV/5hpshVWGbzkKDo6kAEKYAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9Figz/btsKRfYrYuV/5hpshVWGbzkKDo6kAEKYAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9Figz/btsKRfYrYuV/5hpshVWGbzkKDo6kAEKYAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9Figz%2FbtsKRfYrYuV%2F5hpshVWGbzkKDo6kAEKYAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;678&quot; height=&quot;533&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;1185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2) 경로 안내 서비스의 상세 설계&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개략적인 설계에서 좀 더 세분화하여 다음과 같이 여러 역할을 가진 컴포넌트들로 설계할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1829&quot; data-origin-height=&quot;1630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdGYRT/btsKQcVNKc1/kQj76iTRjuve7ViKl5kzK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdGYRT/btsKQcVNKc1/kQj76iTRjuve7ViKl5kzK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdGYRT/btsKQcVNKc1/kQj76iTRjuve7ViKl5kzK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdGYRT%2FbtsKQcVNKc1%2FkQj76iTRjuve7ViKl5kzK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;703&quot; height=&quot;627&quot; data-origin-width=&quot;1829&quot; data-origin-height=&quot;1630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;경로 안내 서비스가 지오코딩 서비스를 호출하여 출발지와 목적지의 위도 경도 페어를 가져온 뒤 경로 계획 서비스를 호출하게 하고, 경로 계획 서비스는 현재 교통 상황이나 도로 상황에 입각해 이동 시간 측면에서 가장 최적화된 경로를 안내하도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;a) 최단 경로 서비스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;출발지와 목적지의 위도 경도 페어와 객체 저장소에 저장된 경로 안내 타일들을 활용해 특정 개수만큼의 최단 경로들을 반환하게 합니다. 즉 교통 상황은 고려 대상이 아닙니다. 경로 안내 타일에 쓰이는 그래프 데이터는 거의 정적인 데이터이므로 캐시를 적극 활용하는 것도 고려할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;b) 예상 도착 시간 서비스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;최단 경로 서비스가 반환한 경로 목록들에 대한 소요 시간 추정치를 구하게 합니다. 이 서비스는 머신 러닝을 통해 과거의 교통 상황을 근거로 한 도착 시간들을 학습하고, 현재의 교통 상황을 입력으로 하여 예상 도착 시간을 계산하도록 설계할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;c) 순위 결정 서비스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;최단 경로 서비스가 구한 경로들에 대해 예상 도착 시간 서비스가 경로별로 예상 도착 시간들까지 구했다면, 순위 결정 서비스를 통해 사용자가 정의한 필터링 조건(도보로만 가기 등)을 적용한 경로들을 얻게끔 합니다. (책에서는 예상 도착 시간들을 구한 뒤 필터링을 적용하나, 개인적으론 필터링을 먼저 적용한 뒤 남은 경로들에 대해 예상 도착 시간을 구하는 것이 연산 횟수가 더 적을 것 같다는 생각이 듭니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/143</guid>
      <comments>https://jofestudio.tistory.com/143#entry143comment</comments>
      <pubDate>Thu, 21 Nov 2024 19:12:02 +0900</pubDate>
    </item>
    <item>
      <title>CAP 이론이란</title>
      <link>https://jofestudio.tistory.com/142</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;분산 시스템의 설계 이론을 말하는 것으로, 시스템이 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Consistency (일관성), Availability (가용성), Partition Tolerance (분할 내성) 이 세 가지를 모두 동시에 완벽히 충족할 수 없다는 것&lt;/b&gt;&lt;/span&gt;을 말합니다. 이 3가지를 모두 만족시킬 수는 없으니, 시스템이 요구하는 것이 무엇인지에 따라 한 가지는 어느 정도 포기해야 한다는 이론입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 항목별 구체적인 설명은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 일관성 (Consistency)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;어떤 노드에 연결하든 모든 클라이언트가 동시에 동일한 데이터를 볼 수 있어야 함을 의미합니다. (일종의 정확성)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;어떤 데이터가 갱신됐다고 치면, 해당 데이터를 조회할 때 어떤 노드에서 조회하든 모든 노드가 동일한 값을 반환해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터가 한 노드에 기록될 때마다 쓰기가 '성공'된 것으로 간주되기 전에 다른 모든 노드에 데이터를 즉시 전달하거나 복제해야 합니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 가용성 (Availability)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모든 요청(읽기 및 쓰기)에 대해 항상 응답할 수 있어야 함을 의미합니다. 즉 언제든지 서비스가 가용해야 한다는 뜻입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터를 요청하는 모든 클라이언트가 하나 이상의 노드가 다운된 경우에도 응답을 받을 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉, 분산 시스템의 모든 작업 노드가 예외 없이 모든 요청에 대해 유효한 응답을 반환해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시스템 일부가 장애를 겪더라도 다른 노드가 요청을 처리하여 응답해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 분할 내성(파티션 허용성이라고도 부름, Partition Tolerance)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시스템에 분할이 생겨도 여전히 시스템은 동작해야 함을 의미합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 네트워크 단절(Network Partition, 예를 들면 일부 노드 간 통신 불가) 상황에서도 시스템은 계속 작동할 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;단일 노드 시스템이라면 분할 내성은 고려하지 않아도 됩니다. 하지만 분산 시스템은 시스템의 요구사항이 어떻든 네트워크 장애가 언제든 발생할 수 있으므로, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;분할 내성은 반드시 챙겨가야 합니다&lt;/b&gt;&lt;/span&gt;. 따라서 일관성과 가용성 중 시스템에 요구사항에 맞춰 하나를 고르는 형태로 시스템을 설계하게 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼 이때 왜 일관성과 가용성을 모두 챙길 수 없다는 것일까요?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시스템에 네트워크 분할이 발생했을 때를 가정해보겠습니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;일관성을 지키려고 한다면 데이터 복제를 기다리는 것이 필요한데, 분리된 노드끼리 데이터를 동기화할 수 없다보니 쓰기 요청에 대해 에러를 반환하거나 요청을 거부해야 하는데, 이 부분이 가용성을 해치는 요소가 되기 때문&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; letter-spacing: 0px;&quot;&gt;입니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;반면 가용성을 챙기겠다고 하면 정상적으로 요청은 처리하게 되지만 분할이 일어난 상태이니 정확한 데이터가 전달된다는 보장은 없게 되고, 따라서 데이터의 일관성은 희생하게 된 것이라고 볼 수 있겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;형태에 따라 CP, AP 시스템으로 다음처럼 분류 가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;1. CP 시스템 (Consistency + Partition Tolerance)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일관성과 분할 내성은 허용, 가용성은 희생하는 구조&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;만약 데이터 쓰기 중 네트워크 장애가 발생하면, 모든 노드가 동일한 데이터를 반환할 때가지 쓰기나 읽기를 지연시킴&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 최신 데이터를 보장하나, 특정 시점엔 요청을 처리하지 못하는 상황(가용성 희생에 따른..)이 발생 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;2. AP 시스템 (Availability + Partition Tolerance)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가용성과 분할 내성은 허용, 일관성은 희생&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;네트워크 장애 시에도 시스템은 항상 가용하나, 최신 데이터가 아닌 오래된 데이터를 반환할 수도 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;네트워크 장애 시에도 쓰기/읽기 요청을 허용하며, 노드 간 동기화는 나중에 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;참고한 레퍼런스&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;a href=&quot;https://www.ibm.com/kr-ko/topics/cap-theorem&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ibm.com/kr-ko/topics/cap-theorem&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;a href=&quot;https://onduway.tistory.com/106&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://onduway.tistory.com/106&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS</category>
      <category>availability</category>
      <category>CAP</category>
      <category>Consistency</category>
      <category>partition tolerance</category>
      <category>설계</category>
      <category>아키텍처</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/142</guid>
      <comments>https://jofestudio.tistory.com/142#entry142comment</comments>
      <pubDate>Sun, 17 Nov 2024 16:06:01 +0900</pubDate>
    </item>
    <item>
      <title>위치 기반 서비스를 위한 아키텍처 설계 - 주변 친구 검색</title>
      <link>https://jofestudio.tistory.com/141</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;※&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;&amp;nbsp;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권 - 챕터 1을 읽고 정리한 글입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;들어가며&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아키텍처에 정답은 없으나, 내 주변 맛집 찾기 서비스와 같이 정적인 위치에 대한 서비스를 설계할 때는 이 공간 데이터들을 DB에 적재해둔 다음 지오해시나 쿼드트리와 같은 공간 데이터 검색을 위한 인덱스를 활용하는 형태의 아키텍처를 구상해볼 수 있습니다. 그러나 내 친구들의 현재 위치와 같이 자주 바뀌는 동적인 위치에 대해 다룰 때는 조금 다른 형태의 아키텍처를 고려할 필요가 있습니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;이번에는 페이스북처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;인근에 있는 친구들의 목록을 보여주는 서비스&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;를 위한 아키텍처를 설계해봅니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;냅다 무대뽀로 설계하면 고려할 것이 많으므로.. 다음과 같은 기능적 요구사항을 가정하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;본인과 친구들의 직선거리를 기준으로, 특정 거리 이하의 친구들의 목록을 볼 수 있어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;목록의 각 항목엔 그 친구까지의 거리, 해당 정보가 갱신된 시각을 표기해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 친구 목록은 친구들의 위치가 바뀔 때마다 갱신되어야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;10분 이상 비활성 상태인 친구들은 목록에 포함하지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;유저들의 위치 히스토리도 별도로 기록해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음과 같은 비기능적 요구사항도 가정하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;각 사용자들의 위치 정보는 30초마다 갱신된다고 하겠습니다. 이때&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: left;&quot;&gt;이들의 위치 변화가 반영되는 데 오랜 시간이 걸리지 않아야 합니다. (책에선 낮은 지연성 = low latency로 표현됩니다)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일부 데이터가 유실돼도 괜찮습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위치 데이터에 강한 일관성은 요구하지 않아도 됩니다. 이 말은 복제본을 사용할 경우 복제본과 원본 DB의 데이터가 순간 달라지는 현상이 발생해도 몇 초까지는 눈감아준다는 말입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;혼자 생각해보기 - HTTP 폴링 기반의 설계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 사용자들의 기본적인 정보를 담는 테이블과 사용자간 친구 관계를 가지는 테이블을 DB에 두는 것을 생각해볼 수 있고, 위치 히스토리는 쓰기 위주의 요청을 많이 받게 될 것이므로 위치 히스토리 DB를 따로 두는 것을 생각해볼 수 있습니다. 사용자들의 현재 위치 정보만 갖고 있는 캐시 서버를 별도로 구축할 수 있고, TTL을 10분으로 설정하면 &quot;해당 캐시 서버에 위치 정보가 있는 사용자 = 활성 상태 사용자&quot;로 취급할 수 있으므로 해당 캐시 서버를 활성 상태인 주변 친구들을 검색하는 데 활용할 수 있겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;사용자들의 위치 변화가 반영되는 데 오랜 시간이 걸리지 않아야 한다&quot;&lt;/b&gt;&lt;/span&gt;라는 비기능적 요구사항으로 인해, 책에서는 웹소켓을 사용해 친구들의 위치 변화를 실시간에 가깝게 처리하는 아키텍처를 선보입니다. 만약 해당 요구사항이 없었다면 개인적으론 30초마다 HTTP 요청을 보내는 방식을 사용하는 아키텍처를 설계할 수 있다고 생각합니다. 이 경우 다음과 같은 개략적인 설계가 가능해보입니다. (사실 시스템 설계를 해본 경험이 없다시피 하다보니.. HTTP를 쓴다면 어떻게 할 수 있을지를 생각해볼 겸 설계해봤습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1511&quot; data-origin-height=&quot;1300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4wQNl/btsKHI8qGrF/qdCnfCYXF6Va6wTOE9lB11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4wQNl/btsKHI8qGrF/qdCnfCYXF6Va6wTOE9lB11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4wQNl/btsKHI8qGrF/qdCnfCYXF6Va6wTOE9lB11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4wQNl%2FbtsKHI8qGrF%2FqdCnfCYXF6Va6wTOE9lB11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;700&quot; data-origin-width=&quot;1511&quot; data-origin-height=&quot;1300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모바일 클라이언트가 30초마다 자신의 위치 정보를 담아 로드밸런서로 HTTP 요청을 보냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로드밸런서가 URL을 보고 인근 친구 검색 서버로 요청을 전달합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;유저별 현재 위치&quot;를 담는 캐시 서버에 내 위치를 갱신하고, 위치 히스토리에 타임스탬프와 내 위치를 기록합니다. 이 작업들은 병렬로 수행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;캐시 서버로부터 내 친구 목록을 조회합니다. 없으면 원본 DB에서 가져와서 캐싱합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;내 친구 목록과 &quot;유저별 현재 위치&quot;를 담는 캐시 서버를 활용해 친구들의 현재 위치를 조회하고, 특정 거리 이하인 애들만 필터링하여 클라이언트에게 응답합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;책에서는 사용자들이 서버로 보내는 위치 정보 변경 전달에 대한 QPS가 334,000 정도의 상황임을 가정하고 있으니, 그에 따른 추가적인 설계가 덧붙여져야 할 것 같습니다. 암튼 이렇게 하면 30초마다 인근에 있는 내 친구들의 목록을 받을 수는 있지만, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;그들의 위치 변화가 내 모바일 기기로 실시간으로 전송되는 것은 아닙니다&lt;/b&gt;&lt;/span&gt;. 책에서는 친구들의 위치 변화를 내 모바일 기기로 실시간에 가깝게 전송할 것을 요구하므로 그에 맞춘 설계안을 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;설계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 개략적인 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;친구들의 위치 변화를 내 기기로 실시간에 가깝게 받으려면 각 기기들을 P2P로 직접 통신하도록 이을 수 있으나, 모바일 기기 특성 상 통신 연결 상태가 좋지 않을 수 있고 일반적인 경우엔 가용한 전력이 한정되어 있음을 고려해야 합니다. 각 기기들이 직접 통신하게 하는 것보다는 중간에 공용 백엔드를 두고 해당 서버를 통해 각자의 위치 정보를 전달하는 방안을 고려할 수 있겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;932&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt2uJ4/btsKIWEnmw6/hVHHopmG8UDNjLaS8KL8uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt2uJ4/btsKIWEnmw6/hVHHopmG8UDNjLaS8KL8uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt2uJ4/btsKIWEnmw6/hVHHopmG8UDNjLaS8KL8uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt2uJ4%2FbtsKIWEnmw6%2FhVHHopmG8UDNjLaS8KL8uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;383&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(1)특정 유저가 본인의 위치를 백엔드로 전달&lt;/b&gt;&lt;/span&gt;하면 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(2)해당 서버가 다른 유저들에게 그 유저의 위치 변화를 전달&lt;/b&gt;&lt;/span&gt;해야 합니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;HTTP는 요청이 와야 응답을 주는 프로토콜이므로 (1)은 HTTP로 처리가 가능하나 (2)는 처리가 힘듭니다. 이때 전이중 통신이 가능한 웹소켓 프로토콜을 사용하도록 서버를 구성한다면 (1)과 (2)를 비교적 쉽게 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 내 위치 변화를 내 친구들한테만 전달하면 되므로, 나와 친구들 사이에 pub / sub 기반의 메시징 기능을 사용하는 것을 고려할 수 있습니다. 대표적으로 Redis pub / sub을 다음과 같이 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1280&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tiqEO/btsKH8ljzQm/pvILnFxcPR4H3LyghQ1fO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tiqEO/btsKH8ljzQm/pvILnFxcPR4H3LyghQ1fO0/img.png&quot; data-alt=&quot;레디스 펍/섭 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tiqEO/btsKH8ljzQm/pvILnFxcPR4H3LyghQ1fO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtiqEO%2FbtsKH8ljzQm%2FpvILnFxcPR4H3LyghQ1fO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;626&quot; height=&quot;291&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;595&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레디스 펍/섭 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹소켓 서버로 수신된 사용자의 위치 정보에 대한 이벤트를 해당 사용자의 채널에 발행(publish)하면, 그 채널을 구독(subscribe)하고 있는 친구들에게 위치 정보 변경이 전달됩니다. 위치 정보 변경을 수신한 친구들이 활성 상태라면 거리를 다시 계산하고, 새로 계산된 거리가 유효 거리라면 웹소켓 연결을 통해 해당 친구의 모바일 기기 단말로 새 위치와 갱신 시각을 보내는 방식으로 설계할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 다음 그림처럼 웹소켓 서버가 스케일 아웃되어 다중화되면 나와 친구가 연결을 맺은 웹소켓 서버가 달라질 수도 있는데요.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;634&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9zZdn/btsLDfQMZFp/5POd4Vx5kKMzFk7K2YAMa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9zZdn/btsLDfQMZFp/5POd4Vx5kKMzFk7K2YAMa1/img.png&quot; data-alt=&quot;서로 연결을 맺은 웹소켓 서버가 달라 메시지 전달이 안됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9zZdn/btsLDfQMZFp/5POd4Vx5kKMzFk7K2YAMa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9zZdn%2FbtsLDfQMZFp%2F5POd4Vx5kKMzFk7K2YAMa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;377&quot; height=&quot;230&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서로 연결을 맺은 웹소켓 서버가 달라 메시지 전달이 안됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis pub / sub 서버를 메시지 전달의 매개체로 사용할 경우 내 위치 정보 변경을 다른 웹소켓 서버로도 전달될 수 있으므로 해당 상황에 대한 대응이 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1163&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xSTZP/btsLCZAFKYz/SkjNKhBLcQme1kB2sEnSW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xSTZP/btsLCZAFKYz/SkjNKhBLcQme1kB2sEnSW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xSTZP/btsLCZAFKYz/SkjNKhBLcQme1kB2sEnSW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxSTZP%2FbtsLCZAFKYz%2FSkjNKhBLcQme1kB2sEnSW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;223&quot; data-origin-width=&quot;1163&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 개략적으로 다음 형태의 아키텍처를 고려할 수 있겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1280&quot; data-origin-height=&quot;1018&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BU6dQ/btsKHqGYyKd/nuey1kwqkc5ZsxiR5s5X8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BU6dQ/btsKHqGYyKd/nuey1kwqkc5ZsxiR5s5X8K/img.png&quot; data-alt=&quot;개략적인 아키텍처 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BU6dQ/btsKHqGYyKd/nuey1kwqkc5ZsxiR5s5X8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBU6dQ%2FbtsKHqGYyKd%2Fnuey1kwqkc5ZsxiR5s5X8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;1018&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1018&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개략적인 아키텍처 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모바일 클라이언트가 30초마다 자신의 위치 정보를 담아 로드밸런서로 요청을 보냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로드밸런서는 해당 클라이언트가 연결을 맺고 있는 웹소켓 서버로 해당 요청을 전달합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹소켓 서버는 수신받은 위치 정보를 위치 히스토리 DB에 저장합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹소켓 서버는 수신받은 위치 정보를 유저별 현재 위치를 담는 캐시 서버에 갱신하고, 웹소켓 연결 핸들러 안의 변수에 해당 위치를 반영합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹소켓 서버는 수신받은 위치 정보를 Redis pub / sub 서버의 사용자 채널에 발행합니다. 3 ~ 5까지의 작업은 병렬 수행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis pub / sub에 발행된 위치 변경 이벤트는 모든 구독자들에게 브로드캐스트됩니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6에서 발생된 위치 변경 이벤트를 받은 웹소켓 연결 핸들러가 있는 웹소켓 서버들은 해당 정보를 바탕으로 새 거리를 계산합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;7에서 계산한 거리가 유효한 거리라면 타임스탬프와 함께 해당 구독자의 모바일 기기로 웹소켓 프로토콜을 통해 전송합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1 ~ 6까지의 사용자가 있다고 가정할 때, 1번의 친구는 2, 3, 4이고 5번의 친구는 4, 6이라 가정해보겠습니다. 이때 5 ~ 8까지의 과정을 도식화해서 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1016&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyD3RB/btsKHgLdmpN/GQOanXP0C7IbXBvXScOyIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyD3RB/btsKHgLdmpN/GQOanXP0C7IbXBvXScOyIK/img.png&quot; data-alt=&quot;위치 변경 전송에 따른 흐름 도식화 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyD3RB/btsKHgLdmpN/GQOanXP0C7IbXBvXScOyIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyD3RB%2FbtsKHgLdmpN%2FGQOanXP0C7IbXBvXScOyIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;541&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위치 변경 전송에 따른 흐름 도식화 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2) 위치 히스토리 DB&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위치 히스토리가 이 서비스에서 주요한 기능은 아니나, 어떤 DB에 저장할 지는 고려하는 것이 좋습니다. 우선 어떤 데이터를 저장할 지를 생각해보면 사용자 식별자와 위도 경도 정보, 타임스탬프값을 저장하면 될 것입니다. 책에서는 QPS가 334,000이므로, 막대한 쓰기 연산을 감당할 수 있어야 하며 대규모의 데이터 저장이 예상되는 만큼 수평적 확장이 가능해야 할 것입니다. 카산드라(Cassandra)는 데이터 쓰기 시 메모리에 먼저 데이터들을 저장하다가 한 번에 디스크로 flush하는 구조라 쓰기 성능이 좋고, 스케일 아웃이 용이하므로 이 요구사항에 적합합니다. 관계형 DB도 사용할 수는 있으나 대규모의 데이터가 예상되는만큼 이 경우는 샤딩을 고려해야겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;상세 설계&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) API 서버의 확장성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;친구 추가, 사용자 정보 상세 조회 등을 담당하는 API 서버는 무상태 서버이므로, CPU 사용률 등에 따라 동적으로 서버 수를 늘리거나 줄이도록 설정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 웹소켓 서버의 확장성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹소켓 서버는 유상태 서버로, 특정 사용자와 연결을 맺으면 사용자와의 통신은 연결을 맺은 서버와만 이루어진다는 특징을 고려하여 확장/축소를 생각해야 합니다. 확장의 경우 API 서버와 마찬가지로 사용률 등에 따라 서버를 늘릴 수 있고, 로드밸런서에서 부하 분산 알고리즘으로 Least-Connections를 사용하면 각 웹소켓 서버들에 맺힌 연결의 개수를 어느 정도 균등히 유지할 수 있습니다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;다만 서버의 규모를 축소시킬 때는 해당 서버에 있던 연결들이 종료될 수 있도록 주의할 필요&lt;/b&gt;&lt;/span&gt;가 있습니다. 이때 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;해당 서버를 로드밸런서가 draining(연결 종료 중)으로 인식하도록 설정&lt;/b&gt;&lt;/span&gt;하면, 해당 서버로는 더 이상 웹소켓 연결이 맺어지지 않도록 할 수 있습니다. 참고로 이를 인플라이트 요청(현재 활성화된 요청)들만 처리하도록 설정한다고도 표현합니다. 암튼.. 그 상태로 충분한 시간이 흐른 뒤 연결들이 모두 종료되면 서버를 제거할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 서버 제거 시 draining을 설정하는 것은 웹소켓 서버에만 국한된 얘기가 아니며, AWS에선 다음과 같이 300초를 디폴트로 draining이 설정되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1276&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nDHP1/btsKITAWjvt/K19hO2wgLcxezRyLuqQYW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nDHP1/btsKITAWjvt/K19hO2wgLcxezRyLuqQYW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nDHP1/btsKITAWjvt/K19hO2wgLcxezRyLuqQYW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnDHP1%2FbtsKITAWjvt%2FK19hO2wgLcxezRyLuqQYW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;244&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) 클라이언트 초기화&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모바일 기기에서 주변 친구 서비스를 최초 사용할 경우, 웹소켓 클러스터에 있는 서버 가운데 하나와 연결을 맺게 됩니다. 최초 연결 시 모바일 기기에서 사용자의 위치 정보를 송신하게 되면 웹소켓 서버는 구체적으로 다음과 같은 작업을 하도록 설계할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위치 정보 캐시에 해당 사용자의 위치 갱신하고 해당 위치를 웹소켓 연결 핸들러 내의 변수에 저장합니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자 DB로부터 해당 사용자의 친구 목록을 가져옵니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위치 정보 캐시로부터 2번에서 가져온 친구들의 위치를 가져옵니다. 위치 정보 캐시에는 TTL을 10분으로 하여 위치 정보들이 저장되므로, 비활성화된 유저들의 위치 정보는 가져오지 않게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각각의 친구 위치들에 대해 거리를 계산하고, 유효한 거리라면 모바일 기기로 전달합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2번에서 가져온 모든 친구들에 대해 Redis pub / sub 채널을 구독합니다. 물론 비활성화 친구에 대한 채널을 유지하는 것은 메모리가 필요하나, 극소량인 데다가 활성화 상태로 전환되기 전까진 CPU나 I/O를 이용하지 않으니 크게 고려하지 않아도 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자의 현재 위치를 Redis pub / sub 채널에 발행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4) 위치 정보 캐시&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 사용자들의 현재 위치 정보를 TTL을 통해 일정 기간 만큼만 보관하므로, 아무리 많아도 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;사용자 전체 수 X 위치 정보를 저장하는 데 필요한 공간&quot;&lt;/b&gt;&lt;/span&gt;이 메모리 사용량의 최대 한도로 유지됩니다. 다만 QPS가 334,000으로 가정된 상황이므로 Redis 서버 한 대가 이를 모두 감당하는 것은 상당히 부담될 수 있습니다. 그러나 사용자별 위치 정보 데이터는 사용자 식별자를 기준으로 비교적 쉽게 샤딩할 수 있고, 가용성을 높이고 싶다면 각 샤드에 보관하는 위치 정보를 standby 노드에 복제하는 방식을 활용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5) Redis pub / sub 서버&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 아키텍처에서 Redis pub / sub 서버는 사용자의 위치 정보 변경을 사용자의 친구들에게 보낼 때의 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;라우팅 계층&lt;/b&gt;&lt;/span&gt;으로서 활용되고 있습니다. 주변 친구 기능을 활용하는 모든 사용자에게 채널이 하나씩 부여되며, 단순한 설계를 위해 모바일 기기는 최초 연결 시 활성화 여부와는 상관없이 모든 친구의 채널을 구독합니다. 이 경우 메모리 사용량과 CPU 사용량을 다음과 같이 고려해 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(a) 메모리 사용량&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis pub / sub은 메모리에 해시 테이블과 링크드 리스트를 통해 채널과 그 채널의 구독자들을 관리합니다. 구독자 한 명에 대해 20Byte의 용량을 사용하고, 주변 친구 기능의 사용자가 1억 명이고 모두에게 채널 하나씩을 할당한다고 한 뒤 각 사용자의 친구들 중 100명만 활성화 상태들이라고 가정하겠습니다. 그러면 1억 X 20Byte X 100명 = 약 200GB의 메모리를 사용하게 되는 것이며, 100GB의 메모리가 있는 서버를 사용할 경우 Redis pub / sub 서버는 2대 정도만 있으면 되겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;(b) CPU 사용량&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;책에서는 사용자들이 위치 정보 변경을 서버로 전달하는 QPS가 334,000인 상황으로, 각 사용자들이 400명 정도의 친구를 가지고 그들 중 10%인 40명이 활성화 상태라고 가정(즉 웹소켓 서버에 그 사용자의 웹소켓 연결 핸들러가 물려있는 상태)하면 Redis pub / sub 서버는 초당 1,400만 건의 위치 정보 변경 이벤트를 전달하게 됩니다. Redis pub / sub 서버 한 대로는 처리하기 힘든 양입니다.. 기가비트 네트워크 카드를 탑재했다고 해도 보수적인 관점에서 1초에 처리 가능한 구독자 수를 100,000 정도로 추정할 수 있다고 하는데(책에서 이렇게 말하는 명확한 근거는 모르겠습니다만..), 그렇다고 해도 1,400만 건 / 10만 = 140대의 서버가 필요합니다. 즉 Redis pub / sub 서버에서 병목이 생기면 메모리가 아닌 CPU 사용량에서 그 이유를 찾을 수 있고, 이에 대한 해결책으로 분산 Redis pub / sub 클러스터를 고려할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;6) 분산 Redis pub / sub 클러스터&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;Redis pub / sub 클러스터를 구성하여&lt;/span&gt; 각 사용자들이 저마다 하나씩 가지는 pub / sub 채널들을 분산시킬 수 있고, 각 채널들은 서로 독립적이므로 사용자 식별자를 기준으로 어떤 서버에 배정될지 정할 수 있습니다. 이때 Redis pub / sub 클러스터의 규모를 확대 또는 축소시키는 경우도 고려를 해야 하는데, 그러기 위해서는&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;&amp;nbsp;Redis pub / sub 서버의 성격이 &quot;무상태&quot;인지 아니면 &quot;유상태&quot;인지를 짚어봐야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;우선 pub / sub 채널에 전송되는 메시지는 구독자들에게 전송된 후 바로 삭제된다는 관점에서는 무상태라고 볼 수 있습니다. 그러나 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;각 pub / sub 서버들은 자신들이 가지는 채널에 대한 상태 정보(ex : 각 채널의 구독자 목록)을 보관하고 있다는 관점에서 보면 유상태&lt;/b&gt;&lt;/span&gt;라고 볼 수 있습니다. 그래서 특정 채널을 담당하던 서버가 없어질 경우 그 채널에 매달려있던 구독자 정보들이 없어질 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;즉 Redis pub / sub 클러스터는 유상태 서버 클러스터로 취급하여 관리&lt;/b&gt;&lt;/span&gt;할 필요가 있습니다. 현재 가용한 pub / sub 서버들의 목록을 유지하고 이 서버들에서 발행한 변경 내역들을 구독할 수 있는 기능을 가진 컴포넌트를 별도로 두는 것을 고려할 수 있고, 대표적으론 주키퍼라는 분산 코디네이션 서비스를 쓸 수 있습니다. 이때 가용한 pub / sub 서버들을 해시 링 형태로 보관하고, 메시지를 발행할 채널 또는 구독할 채널이 있는 pub / sub 서버를 정해야 할 때 이 링을 참조하도록 할 수 있습니다. (이 글의 제일 하단에서 다루겠습니다). 이를 통해 웹소켓 서버가 특정한 채널에 위치 정보 변경을 발행하는 과정을 다음과 같이 나타낼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1410&quot; data-origin-height=&quot;1341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NsbyO/btsKHLYlbSs/gjJTLlUv02aa3XWHJxzD71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NsbyO/btsKHLYlbSs/gjJTLlUv02aa3XWHJxzD71/img.png&quot; data-alt=&quot;채널 2에 메시지를 발행하는 경우!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NsbyO/btsKHLYlbSs/gjJTLlUv02aa3XWHJxzD71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNsbyO%2FbtsKHLYlbSs%2FgjJTLlUv02aa3XWHJxzD71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;713&quot; height=&quot;678&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;1341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채널 2에 메시지를 발행하는 경우!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹소켓 서버는 해시 링을 참조해 메시지를 발행할 pub / sub 서버를 결정합니다. 이 과정에서 주키퍼를 활용하나, 성능을 높이고 싶다면 해시 링 사본을 웹소켓 서버 자체에 캐시하는 방법을 사용 가능합니다(즉 주키퍼를 참조하는 네트워크 i/o가 없어짐). 그러나 이 경우는 해시 링 원본에 구독 관계를 설정, 사본의 상태를 항상 원본과 동일하게 유지하도록 추가 설계가 필요합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹소켓 서버가 해당 pub / sub 서버가 관리하는 채널에 메시지를 발행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;그럼에도.. Redis pub / sub 클러스터와 같은 유상태 서버 클러스터의 규모를 확대하거나 축소하는 것은 운영 부담과 위험이 큰 작업인 것은 여전합니다. 따라서.. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;어지간하면 처음부터 큼지막하게 오버 프로비저닝을 하는 것이 보통입니다.&amp;nbsp; 그러나 정말 어쩔 수 없이 규모 변경을 불가피하게 진행해야 할 경우 시스템 부하가 가장 낮은 때(ex : 새벽..)에 하는 것이 좋습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;※ &lt;/span&gt;Consistent Hashing와 Hash ring&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;위에서 pub / sub 채널들은 서로 독립적이므로 사용자 식별자 등을 기준으로 어떤 서버에 채널을 배정해야 할 지 정할 수 있다고 했습니다. 이와 같이 분산 시스템에서 특정한 값이 해시값에 따라 어느 노드로 갈지 정하는 경우 대표적으로 모듈러 연산을 활용 가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ex) 3으로 나눈 나머지에 따라 노드를 배정한다고 하면..&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1번 : 1번 노드에 배정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2번 : 2번 노드에 배정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3번 : 0번 노드에 배정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4번 : 1번 노드에 배정..&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;하지만 이 방법은 노드의 수가 변하면 특정 노드에 있던 데이터들이 여전히 그 노드에 남아있을지는 보장되지 않으므로 기존 데이터들을 재분배해야 하는 문제가 있습니다. 이때 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;안정 해시(Consistent Hashing)을 사용하면 노드 수가 변해도 재분배해야 하는 데이터를 적은 수로 가져갈 수 있으며, 대표적인 방법이 해시 링&lt;/b&gt;&lt;/span&gt;입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1196&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c783jQ/btsKHH2Vxm4/Kxz1s4LAdN9JzhkLT6iKEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c783jQ/btsKHH2Vxm4/Kxz1s4LAdN9JzhkLT6iKEK/img.png&quot; data-alt=&quot;출처 : https://www.toptal.com/big-data/consistent-hashing&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c783jQ/btsKHH2Vxm4/Kxz1s4LAdN9JzhkLT6iKEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc783jQ%2FbtsKHH2Vxm4%2FKxz1s4LAdN9JzhkLT6iKEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;585&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://www.toptal.com/big-data/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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해시 링은 이미지처럼 각 노드(&quot;키&quot;로도 이해 가능하며 이미지에선 A, B, C)와 데이터(Jane, Kate 등)를 특정 해시값으로 변환해 링 위에 배치하고, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;데이터들이 놓인 위치(해시값 범위)에 따라 어느 노드에 배정될지를 결정&lt;/b&gt;&lt;/span&gt;하는 방식입니다. 만약 위 이미지에서 C가 사라진다고 하면 C에 붙어있던 John과 Steve만 A로 붙여주면 되고, 특정 노드가 추가된다고 하면 해당 범위에 있는 애들만 다시 붙여주면 됩니다. 따라서 노드 수에 변화가 생겼을 때 모든 데이터를 재분배하지 않고 특정 범위에 해당하는 데이터들만 재분배해줄 수가 있게 됩니다.&lt;br /&gt;&lt;br /&gt;다만 단점도 있는데요. 데이터를 균등히 저장하지 못할 수 있다는 단점(해시 특성상 어쩔 수 없다고 생각됩니다)과 노드가 삭제되는 순간에는 인접한 다른 노드로 삭제된 노드에 붙어있던 데이터들이 달라붙게 되어 그 노드에 대한 부하가 커질 수 있고, 최악의 경우 이게 연쇄적인 노드 죽이기(?)가 될 수 있다는 단점이 있습니다. 이는 실제 노드가 여러 개의 논리적인 virtual node들을 만들고, 얘네들을 링 위에 무작위하게 뿌리는 방식으로 어느 정도 보완 가능합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS</category>
      <category>pub sub</category>
      <category>가상면접사례로 배우는 대규모 시스템 설계 기초</category>
      <category>레디스</category>
      <category>웹소켓</category>
      <category>주변 친구</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/141</guid>
      <comments>https://jofestudio.tistory.com/141#entry141comment</comments>
      <pubDate>Thu, 14 Nov 2024 01:38:47 +0900</pubDate>
    </item>
    <item>
      <title>위치 기반 서비스를 위한 아키텍처 설계 - 근접성 서비스</title>
      <link>https://jofestudio.tistory.com/140</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;※&lt;/span&gt;&amp;nbsp;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권 - 챕터 1을 읽고 정리한 글입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;들어가며&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근접성 서비스(Proximity Service)란 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;특정 위치를 기반으로 가까운 시설을 찾는 서비스&lt;/b&gt;&lt;/span&gt;를 의미합니다. 예를 들어 주변 맛집 찾기 서비스를 설계한다고 할 때, 2차원 형태(위도, 경도)로 표현되는 공간 데이터를 빠르게 검색하는 기술에 대한 이해를 바탕으로 아키텍쳐를 설계할 필요가 있습니다. 공간 데이터를 검색하는 기술들을 살펴보고, 그 중 지오해시(Geohash)를 기반으로 한 아키텍처 설계 방법에 대해 소개하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;공간 데이터 검색 기술&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음과 같이 특정 위치와 반경을 기반으로 주변의 시설들을 검색하는 방법들에 대해 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;598&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bV2zB2/btsKxKsvt18/QJm1orHaegFLBWN9bqtFpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bV2zB2/btsKxKsvt18/QJm1orHaegFLBWN9bqtFpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bV2zB2/btsKxKsvt18/QJm1orHaegFLBWN9bqtFpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbV2zB2%2FbtsKxKsvt18%2FQJm1orHaegFLBWN9bqtFpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;370&quot; height=&quot;376&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 가장 단순한 공간 데이터 검색 : 2차원 검색&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가장 단순한 방법입니다. 주어진 반경값을 토대로 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;검색해야 하는 위도와 경도의 범위를 계산하여 검색에 활용하는 방&lt;/span&gt;법&lt;/b&gt;&lt;/span&gt;입니다. 그러나 별도의 인덱스 설정이 없으면 테이블 전체를 풀 스캔해야 하는 단점이 있습니다. 위도와 경도에 인덱스를 달아둔다고 해도, 각 인덱스를 통해 얻어낸 두 집합의 교집합을 구해야 하는 데서 많은 비용이 발생합니다. 이런 문제들은 기본적으론 DB 인덱스가 한 차원의 검색 속도를 개선시키는 것에서 비롯되므로, 2차원의 공간 데이터에 대한 인덱스 생성 방법을 고려할 필요가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;공간 데이터에 대해 인덱스를 만드는 방법은 다음과 같이 분류 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;Hash 기반&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;균등 격자 (even grid)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지오해시 (Geohash)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;카르테시안 계층 (Cartesian tiers)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;트리 기반&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쿼드트리 (Quadtree)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구글 S2&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;R-tree&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 기술들의 세부 구현 방법은 서로 다르지만, 기본적으로는&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;공간을 여러 작은 영역들로 분할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;공간 데이터들을 분할된 영역들로 매핑 (2차원 데이터가 1차원 데이터로 매핑되는 효과)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;분할된 영역&quot;들을 빠르게 검색&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하는 형태로 인덱스가 활용된다고 보면 됩니다. 여기서는 균등 격자와 지오 해시, 쿼드 트리 및 R-tree에 대해 소개하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) Hash 기반 공간 데이터 검색 : 균등 격자&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2차원 공간을 다음과 같이 균일한 크기를 가진 격자로 나누는 접근법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;652&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNgf80/btsKzn3RzKe/4YZXBLgBer13ilsgLyIuw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNgf80/btsKzn3RzKe/4YZXBLgBer13ilsgLyIuw1/img.png&quot; data-alt=&quot;균등 격자 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNgf80/btsKzn3RzKe/4YZXBLgBer13ilsgLyIuw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNgf80%2FbtsKzn3RzKe%2F4YZXBLgBer13ilsgLyIuw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;330&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;균등 격자 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 격자들에 1부터 시작하는 식별자 번호를 할당할 수 있고, 공간 데이터들을 각 격자들로 매핑시켜 활용할 수 있게 됩니다. 그러나 뒤에서 소개할 다른 방법들과는 달리 격자 식별자를 할당하는 방법에 명확한 체계가 있는 방식은 아니기 때문에, 특정 격자 주변에 있는 격자를 찾는 것이 어렵다는 단점이 있습니다. 우리의 목적인 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;특정 위치와 반경을 기반으로 주변의 시설을 검색하기&quot;&lt;/b&gt;&lt;/span&gt;를 위해서는 인접한 격자를 쉽게 찾을 수 있어야 하지만 그 부분에 제약이 걸리는 것으로 볼 수 있습니다. 또한 각 격자별로 갖는 공간 데이터들의 분포가 균등하지 않다는 단점도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3) Hash 기반 공간 데이터 검색 : 지오해시&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;균등 격자와 비슷하게 2차원 공간을 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;균일한 크기를 가진 격자로 나누고 격자들에 식별자를 할당합니다. 다만 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;식별자 할당에 체계가 있다는 차이점&lt;/b&gt;&lt;/span&gt;이 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;920&quot; data-origin-height=&quot;1179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq48Lz/btsKyBaLR05/RyKxjz0K7zknyDu6Je97HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq48Lz/btsKyBaLR05/RyKxjz0K7zknyDu6Je97HK/img.png&quot; data-alt=&quot;지오해시 개념 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq48Lz/btsKyBaLR05/RyKxjz0K7zknyDu6Je97HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq48Lz%2FbtsKyBaLR05%2FRyKxjz0K7zknyDu6Je97HK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;471&quot; height=&quot;604&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;1179&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;지오해시 개념 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;2차원 공간을 그림처럼 4개로 나눈 후, 위도 경도의 범위를 기준으로 4개 영역에 비트값을 할당합니다. 다시 각 격자를 4개로 나눈 후, 각 영역에 비트값을 추가하는 과정을 원하는 정밀도(즉 격자의 크기)가 나올 때까지 반복합니다. 이렇게 나온 비트값을 base 32 표현법으로 인코딩해 나온 문자열을 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;지오해시&lt;/b&gt;&lt;/span&gt;라고 부릅니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;ex) 비트값이 &quot;1001 11010 01001 10001 11111 11110&quot; 이면 지오해시는 &quot;9q9hvu&quot;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;지오해시의 길이가 길수록 정밀도가 높은 것(사이즈가 작은 격자들로 분할된 것)을 의미&lt;/b&gt;&lt;/span&gt;하며, 지오해시의 길이에 따라 총 12 레벨로 분류할 수 있습니다. 길이 1짜리인 1레벨은 지구 전체에 해당(즉 격자 1개)하고 길이 12짜리인 12레벨은 격자 하나의 사이즈가 3.7cm X 1.9cm입니다. 일반적인 경우 4 ~ 6레벨을 사용하며, 사용자가 지정한 반경으로 그린 원을 덮는 최소 크기 격자를 만드는 지오해시 길이를 최적 정밀도로 계산할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;ex) 지정한 반경이 1km일 때, 지오해시 레벨이 5인 격자의 크기가 4.9km X 4.9km으로 지정된 반경으로 그린 원을 덮을 수 있는 최소 크기 격자가 됩니다. 레벨 6짜리 격자의 크기는 1.2km X 0.6km입니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;앞서 살펴봤듯이, 지오해시는 격자를 4개로 나눈 후 원래 가지던 비트값 뒤에 01, 11, 00, 10을 추가하는 식으로 식별자 할당이 진행됩니다. 따라서 이 비트값들을 base 32로 인코딩한 지오해시값들은 비슷할 수밖에 없고, 이를 통해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;공통접두어가 긴 격자들은 인접해있다&quot;를 도출&lt;/b&gt;&lt;/span&gt;할 수 있습니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;또한 뒤에 붙어있는 비트값을 떼주면, 해당 격자를 포함하는 좀 더 넓은 격자로 범위를 확장시킬 수 있습니다. 따라서 지오해시는 이 점들을 활용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;범위 기반 지역 검색을 쉽게 할 수 있게 됩니다&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;.&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;다만 하나의 격자만 볼 때는 균등 격자와 마찬가지로 각 격자별로 갖는 공간 데이터들의 분포가 균등하지 않다는 단점은 남아있으며, 공통접두어가 긴 격자들은 인접하다라는 명제는 성립하나&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;인접한 격자들은 공통접두어가 같다&quot;는 성립하지 않음에 유의&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해야 합니다&lt;/span&gt;&lt;/span&gt;. 이유는 다음 그림과 같이 인접한 격자임에도 비트값들은 서로 완전히 다를 수 있기 때문입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;440&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnKzCr/btsKyHhNhXZ/Hgfw6fPpLTBZ7kr6Rg3rh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnKzCr/btsKyHhNhXZ/Hgfw6fPpLTBZ7kr6Rg3rh0/img.png&quot; data-alt=&quot;파란색은 아예 비트 구성이 다르다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnKzCr/btsKyHhNhXZ/Hgfw6fPpLTBZ7kr6Rg3rh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnKzCr%2FbtsKyHhNhXZ%2FHgfw6fPpLTBZ7kr6Rg3rh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;357&quot; height=&quot;295&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파란색은 아예 비트 구성이 다르다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4) 트리 기반 공간 데이터 검색 : 쿼드트리&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지오해시는 격자들에 할당된 식별자들을 DB에 저장해 가지는 방식이었다면, 쿼드트리는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;격자를 재귀적으로 4개로 쪼개가며 만든 트리를 메모리에 둔 채로 활용하는 방식&lt;/b&gt;&lt;/span&gt;입니다. 구체적으로는 트리의 리프노드들에 담긴 공간 데이터가 내가 원하는 수(k) 이하가 될 때까지 분할하며, 리프노드에는 해당 격자에 포함된 공간 데이터의 정보들을 갖도록 구성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;952&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdn187/btsKy0Vz8jH/mkomnd1pAcxuGAoAhzJCt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdn187/btsKy0Vz8jH/mkomnd1pAcxuGAoAhzJCt1/img.png&quot; data-alt=&quot;쿼드트리 개념 (https://pigbrain.github.io/datastructure/2016/01/01/QuadTree_on_DataStructure)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdn187/btsKy0Vz8jH/mkomnd1pAcxuGAoAhzJCt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdn187%2FbtsKy0Vz8jH%2Fmkomnd1pAcxuGAoAhzJCt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;603&quot; height=&quot;418&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿼드트리 개념 (https://pigbrain.github.io/datastructure/2016/01/01/QuadTree_on_DataStructure)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모든 격자를 균일한 사이즈로 해야했던 지오해시와는 달리, 쿼드트리는 맘만 먹으면 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;원하는 영역에 대해 세밀하게 격자를 분할하는 것이 쉽습니다&lt;/b&gt;&lt;/span&gt;. 또한 트리 구성 시 특정 숫자(k)를 기준으로 만들어 나가는 점을 활용해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;현재 내 위치에서 가까운 공간 데이터 x개 찾기&quot;&lt;/b&gt;&lt;/span&gt;를 쉽게 할 수 있게 됩니다. 하지만 트리 구조인 만큼 공간 데이터 추가 / 삭제로 인한 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;인덱스 변경이 좀 더 까다롭고&lt;/b&gt;&lt;/span&gt;, 서버 시작 시 트리를 구축해서 메모리에 둬야 하기 때문에 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;서버 시작 시간이 길어질 수 있음&lt;/b&gt;&lt;/span&gt;을 유의해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;설계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위에서 살펴본 기술 중, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;지오해시&lt;/b&gt;&lt;/span&gt;를 사용할 때의 일반적인 설계 방법에 대해 살펴보겠습니다. 특정 위치와 반경을 기준으로 주변 시설을 조회하는 기능과 특정 시설의 상세 정보를 제공하는 서비스로, 다음과 같은 기능적 요구사항이 있다고 가정합니다&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자의 경도와 위도, 반경에 매치되는 시설 목록을 반환해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시설 정보가 추가/갱신/삭제될 수 있으나 실시간으로 반영될 필요는 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시설의 상세 정보를 조회할 수 있어야 함&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;주변 시설을 빠르게 검색 가능해야 하고 트래픽이 급증해도 감당할 수 있어야 한다는 비기능적 요구사항&lt;/b&gt;&lt;/span&gt;이 있다고 하겠습니다. 또한&amp;nbsp; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1초에 5,000번 정도의 검색이 발생하는 읽기 연산 위주&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;의 시스템으로&lt;/span&gt;&lt;/span&gt;&amp;nbsp;가정하며, 다음과 같은 API들이 있다고 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 106px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 10.1163%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Method&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;API&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 10.1163%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;GET&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; height: 19px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;/v1/search/nearby&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인자로 위도, 경도, 반경을 받은 뒤 검색 기준에 맞는 사업장 목록 반환&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.1163%; height: 17px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;GET&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; height: 17px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;/v1/businesses/:id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특정 사업장의 상세 정보 반환&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.1163%; height: 17px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;POST&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; height: 17px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;/v1/businesses&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;신규 사업장 추가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.1163%; height: 17px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PUT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; height: 17px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;/v1/businesses/:id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사업장 상세 정보 갱신&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.1163%; height: 17px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DELETE&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; height: 17px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;/v1/businesses/:id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특정 사업장 정보 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 서버&amp;nbsp; 및 로드밸런서 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 시스템을 위치 기반으로 시설들을 검색하는 서비스(LBS)와 시설 서비스(시설 상세조회, 등록 등을 담당)로 구분할 수 있습니다. 사용자가 보내는 요청들을 각각의 알맞는 서비스로 전달하기 위해 로드밸런서에서 URL 경로를 분석해 다음과 같이 설정해줄 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1142&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UiwSr/btsKxP1Cp0s/ihcAD1AsfUapcJH1IBfHlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UiwSr/btsKxP1Cp0s/ihcAD1AsfUapcJH1IBfHlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UiwSr/btsKxP1Cp0s/ihcAD1AsfUapcJH1IBfHlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUiwSr%2FbtsKxP1Cp0s%2FihcAD1AsfUapcJH1IBfHlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;444&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 LBS와 시설 서비스는 이전 요청의 상태나 데이터를 유지하지 않는 무상태 서비스이므로 확장성이 좋습니다. 따라서 트래픽이 몰릴 때는 서버를 추가하여 대응하고, 트래픽이 줄어들면 서버를 삭제하도록 유연하게 구성할 수 있습니다. 또한, 클라우드에 시스템을 띄운다면 여러 지역 또는 여러 가용 영역에 둠으로써 고가용성도 확보할 수 있겠습니다. 여러 지역에 둘 경우 DNS 라우팅을 통해 사용자와 가까운 서버에서 트래픽을 처리하도록 하여 응답 시간을 줄이는 효과 등을 기대할 수 있으며, 한 지역 안에서도 여러 가용 영역을 활용한 부하 분산을 기대할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2) 데이터베이스 설계&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 시스템은 앞서 가정한대로 읽기 연산이 많이 발생하는 특징이 있습니다. 따라서 데이터베이스를 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;master-slave 형태로 구성&lt;/b&gt;&lt;/span&gt;하여 master가 쓰기 요청을, slave가 읽기 연산을 처리하도록 구성하여 부하를 어느 정도 분산시키도록 구성할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 테이블을 위치 정보 관련 연산의 효율을 위해 시설의 상세 정보를 담는 테이블(business 테이블)과 공간 인덱스 테이블(지오해시값과 시설id만 가지는)로 분리할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;먼저 business 테이블의 경우 시설 데이터가 방대하다면 한 서버에 담기지 못할 수 있으므로, 샤딩을 통해 수평적으로 분리 확장시킬 수 있겠습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;공간 인덱스 테이블은 서버 한 대에 충분히 수용 가능할 것으로 예상되나, 읽기 연산이 많은 경우 가용한 리소스(CPU, 네트워크 대역폭 등)의 한계가 있으므로 부하 분산이 필요할 수 있는데요. 보통 부하 분산이 필요한 경우 Replica를 늘리는 것과 샤딩을 고려할 수 있으나,&amp;nbsp; 지오해시는 격자별 공간 데이터 분포가 고르지 않아 샤딩이 까다롭고 공간 인덱스 테이블은 서버 한 대에 충분히 수용 가능할 것으로 예상되므로 Replica를 활용하는 것이 더 나은 방안이 될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) 캐시 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다중 서버 환경이므로 로컬 캐시보다는 캐시 서버를 구축하는 형태의 글로벌 캐시를 사용하는 것이 좋습니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가장 직관적으로 떠올릴 수 있는 캐시 키는 사용자의 위도 경도입니다. 그러나 위도 경도는 사용자가 조금만 움직여도 달라지기 때문에, 캐시 키로 설정할 경우 캐시 히트가 발생하길 기대하기가 매우 어렵습니다. 이때 아까 배웠던 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;지오해시나 쿼드트리 등을 사용한다면 같은 격자 내의 시설들이 같은 격자 식별자 값을 갖도록 만들 수 있으므로 이 문제를 효과적으로 해결 가능&lt;/b&gt;&lt;/span&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구체적으로 다음 데이터들을 캐시에 보관하도록 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 48.7209%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.68257%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;키&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.9639%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;값&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.68257%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지오해시&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.9639%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 격자 내의 시설 id 목록&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.68257%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시설 id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.9639%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시설 상세 정보 객체&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특정 지오해시에 대응되는 시설 목록을 요청받은 경우, 다음과 같이 설계할 수 있겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;캐시 먼저 조회&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;없으면 DB에서 해당 지오해시에 대응되는 시설 id 목록 가져와서 캐싱&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시설 서비스를 통해 시설의 상세 정보를 조회하는 경우도 위 플로우대로 설계할 수 있겠습니다. 새로운 시설을 추가하거나 기존 시설 정보를 편집 또는 삭제하는 경우, DB를 그에 맞춰 갱신하고 캐시에 보관된 항목은 무효화시킬 수 있습니다. 만약 검색 반경으로 가능한 값들이 정해진 경우, 각 반경에 맞춘 적절한 지오해시 레벨들을 계산 가능하므로 해당하는 레벨에 대한 검색 결과들을 미리 캐싱해둘 수도 있겠습니다. 또한 고가용성 보장을 위해 캐시 서버도 클러스터를 구성하는 것을 고려할 수 있으며, 서버들을 여러 지역에 둔 것과 마찬 가지로 캐시 서버 클러스터를 각 지역별로 구성하여 가까운 캐시 서버를 사용하도록 구성할 수 있겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4) 최종 설계도&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 설명한 것들을 토대로 다음과 같은 아키텍처를 설계할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1405&quot; data-origin-height=&quot;1384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlepST/btsKBlLlriz/VZd8S8k93dY07ct0KgSiRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlepST/btsKBlLlriz/VZd8S8k93dY07ct0KgSiRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlepST/btsKBlLlriz/VZd8S8k93dY07ct0KgSiRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlepST%2FbtsKBlLlriz%2FVZd8S8k93dY07ct0KgSiRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1405&quot; height=&quot;1384&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;1384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 아키텍처에서의 플로우는 다음과 같이 설계할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;a) 주변 시설들을 검색하는 경우&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트는 위치(위도, 경도)와 검색 반경을 로드밸런서로 전송합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로드밸런서는 URL을 보고 해당 요청을 LBS로 보냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;LBS는 요청에 포함된 위치와 반경 정보를 토대로 최적 정밀도를 갖는 지오해시 길이를 계산합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;LBS는 사용자의 위치를 담는 지오해시와 인접한 지오해시들을 계산하여 목록으로 가집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;LBS는 목록에 있는 각 지오해시값에 대해, 글로벌 캐시 서버로부터 해당 지오해시에 매핑된 시설들의 id목록을 가져옵니다. 만약 캐싱된 게 없다면 DB의 공간 인덱스 테이블에서 가져와서 캐싱합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 시설id들에 대해, 글로벌 캐시 서버로부터 해당 id에 매핑된 상세 정보들을 가져옵니다. 만약 캐싱된 게 없다면 DB의 business 테이블에서 가져와서 캐싱합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;b) 시설의 상세정보를 조회하는 경우&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트는 조회할 시설의 id를 로드밸런서로 전송합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로드밸런서는 URL을 보고 해당 요청을 시설 서비스 서버로 보냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시설 서비스 서버는 글로벌 캐시 서버로부터 해당 시설id에 대한 상세 정보를 가져옵니다. 만약 캐싱된 게 없다면 DB의 business테이블에서 가져와서 캐싱합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시설 정보가 추가/갱신/삭제된 경우, 해당 정보가 실시간으로 반영될 필요는 없음을 처음에 가정했으므로 캐시에 보관된 시설 정보의 갱신은 밤 사이에 배치를 수행하는 방식으로 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS</category>
      <category>공간데이터</category>
      <category>위치기반서비스</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/140</guid>
      <comments>https://jofestudio.tistory.com/140#entry140comment</comments>
      <pubDate>Thu, 7 Nov 2024 03:38:51 +0900</pubDate>
    </item>
    <item>
      <title>한계를 뛰어넘는 자바의 마법, Virtual Thread 뜯어보기</title>
      <link>https://jofestudio.tistory.com/139</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://youtu.be/pt7cNy7KlpE?si=OUiwPpv_nywXW7ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/pt7cNy7KlpE?si=OUiwPpv_nywXW7ko&lt;/a&gt;&lt;/span&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=pt7cNy7KlpE&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/b7O6dm/hyXed8qsfY/EcfsNht3kpPI4BQJrxopx1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/bfm1YU/hyXd7AnITO/8YHSR5rBY4bzk6ZNOO2o3k/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;가장 쉽게 이해하는 Java Virtual Thread&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/pt7cNy7KlpE&quot; width=&quot;480&quot; height=&quot;360&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;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;최근 사내에서 Virtual Thread에 대한 기술 세미나를 열었는데요. 사내 세미나 한 번으로 해당 내용을 묵혀두기는 아까워서 유튜브 영상으로도 공유하고.. 블로그에도 남겨볼려고 합니다. 다만 사내 세미나와 영상에서는 비IT 직무 인원들도 이해할 수 있게끔 최대한 비유를 들며 쉽게 설명하려고 했던 것과 다르게 블로그에는 최대한 기술적으로(?) 작성해보고자 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread는 JDK21버전부터 공식 Feature로 포함된 기술로, 블록체인이나 AI같이 산업의 변화를 이끌고 있는 기술보다는 성능 최적화를 위해 쓰이는 뒷단의 기술에 가깝습니다. Java를 기반으로 하는 플랫폼이 기존에 어떤 컨셉의 모델을 사용해왔고, 이게 어떤 한계가 있었고, 이를 바탕으로 Virtual Thread가 어떤 목적으로 나온 기술인지 이번 포스트에서 살펴보겠습니다. 그리고 Virtual Thread의 Context Switching, Throughput에 대한 테스트 내용과 사용 시 주의사항에 대해 소개하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;목차는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Thread per Request &amp;amp; Thread Pool (Java가 전통적으로 사용해 온 방식)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Reactive Programming의 등장 및 한계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread의 개념과 장점&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread의 동작 방식 및 원리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread 테스트 - Context Switching&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread 테스트 - Throughput&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread 주의사항 - Pinning&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. Thread per Request &amp;amp; Thread Pool&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring으로 대표되는 Java 기반 플랫폼은 사용자로부터 하나의 Request가 들어오면 하나의 스레드로 이를 처리하는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Thread per Request&lt;/b&gt;&lt;/span&gt;라는 방식을 옛날부터 사용해왔습니다. 그러나 매 Request마다 새로운 스레드를 생성하는 것은 비용 부담이 크기 때문에, 미리 일정량의 스레드를 만들어두고 Request가 들어올 때마다 만들어둔 스레드에게 해당 Request의 처리를 맡기는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Thread Pool&lt;/b&gt;&lt;/span&gt;이란 개념을 사용해왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1184&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZNMlH/btsJWhqXIm7/0T0MBeAaKSQlnOegig9p3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZNMlH/btsJWhqXIm7/0T0MBeAaKSQlnOegig9p3k/img.png&quot; data-alt=&quot;Thread Pool 도식화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZNMlH/btsJWhqXIm7/0T0MBeAaKSQlnOegig9p3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZNMlH%2FbtsJWhqXIm7%2F0T0MBeAaKSQlnOegig9p3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1184&quot; height=&quot;629&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Thread Pool 도식화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 시간이 흐르며 서비스의 규모가 커지고 Request의 양이 늘어남에 따라, 서비스의 처리량을 늘려야 하는 미션이 생기게 됩니다. 가장 심플한 방법은 스레드풀의 사이즈, 즉 스레드의 개수를 늘리는 것이지만 공간적인 한계 때문에 스레드의 개수가 제한될 수밖에 없었고, 이는 서비스의 처리량이 일정 수준으로 한정되는 결과로 이어졌습니다. 그리고 Java는 자바 스레드(유저 스레드)와 커널 스레드가 1 : 1로 매핑되는 스레딩 모델을 사용하기 때문에, 스레드 간 전환은 곧 커널 스레드간의 전환을 의미하고 이는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Context Switching 비용 부담이 점점 부각&lt;/b&gt;&lt;/span&gt;되는 문제점을 보이게 됐습니다. 또한 자바 스레드가 I/O 작업 등으로 인해 Blocking되면 매핑된 커널 스레드까지 함께 Blocking됐기 때문에, 만들어둔 스레드들이 실질적으로 task를 수행하는 시간보다 대기하는 시간 즉 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;i&lt;/b&gt;&lt;b&gt;dle time이 훨씬 더 많아지는 문제점&lt;/b&gt;&lt;/span&gt;도 부각되기 시작했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1200&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUHLBO/btsJWrAkydQ/TMrjmJcYMX1s6GFYcw9sKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUHLBO/btsJWrAkydQ/TMrjmJcYMX1s6GFYcw9sKK/img.png&quot; data-alt=&quot;자바 스레드가 Blocking되면 커널 스레드도 Blocking되어 처리량에 한계가 생기게 됩니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUHLBO/btsJWrAkydQ/TMrjmJcYMX1s6GFYcw9sKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUHLBO%2FbtsJWrAkydQ%2FTMrjmJcYMX1s6GFYcw9sKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;629&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자바 스레드가 Blocking되면 커널 스레드도 Blocking되어 처리량에 한계가 생기게 됩니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 상황에서 어쨌거나 처리량을 올려야 했고, 가장 단순한 방법인 &quot;스레드 늘리기&quot;는 공간적 제약으로 할 수 없는 상황입니다. 전통적으로 사용해 온 Thread per Request는 하나의 스레드가 하나의 요청을 처리하는 방식인데, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;하나의 스레드가 두 개 이상의 요청을 동시에 처리하게 한다면 처리량이 올라가지 않는가?&quot;&lt;/b&gt;&lt;/span&gt;라는 아이디어가 나오게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #555555; text-align: start;&quot;&gt;※&lt;/span&gt; 이 때의 &quot;동시&quot;는 시간적으로 동시에 한다는 개념이 아닌, 컴퓨터 공학에서 말하는 동시성의 개념으로 보면 됩니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1121&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qoJHU/btsJWUvlvFO/QaSg0khT9xuHNLH2morVyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qoJHU/btsJWUvlvFO/QaSg0khT9xuHNLH2morVyK/img.png&quot; data-alt=&quot;Thread per Request : 하나의 스레드가 하나의 일을 처리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qoJHU/btsJWUvlvFO/QaSg0khT9xuHNLH2morVyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqoJHU%2FbtsJWUvlvFO%2FQaSg0khT9xuHNLH2morVyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;781&quot; height=&quot;317&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;455&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Thread per Request : 하나의 스레드가 하나의 일을 처리&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vwXQ0/btsJWVgIeMi/WJ7XxCVJe1lhBnh8zKLZAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vwXQ0/btsJWVgIeMi/WJ7XxCVJe1lhBnh8zKLZAK/img.png&quot; data-alt=&quot;하나의 스레드가 두 개 이상의 Request을 동시에 처리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vwXQ0/btsJWVgIeMi/WJ7XxCVJe1lhBnh8zKLZAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvwXQ0%2FbtsJWVgIeMi%2FWJ7XxCVJe1lhBnh8zKLZAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;767&quot; height=&quot;311&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;455&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하나의 스레드가 두 개 이상의 Request을 동시에 처리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 아이디어를 바탕으로 Reactive Programming이 등장하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. Reactive Programming의 등장 및 한계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;하나의 스레드가 동시에 두 개 이상의 Request를 처리하게 한다&quot;라는 생각을 바탕으로 등장한 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;비동기 + Non-Blocking 방식의 프로그래밍 패러다임&lt;/b&gt;&lt;/span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;755&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1YmD5/btsJXJGEYA9/xDyFLgZ63jco6jrAob39qK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1YmD5/btsJXJGEYA9/xDyFLgZ63jco6jrAob39qK/img.png&quot; data-alt=&quot;[동기 비동기 차이] 이미지 출처 : https://wikidocs.net/228265&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1YmD5/btsJXJGEYA9/xDyFLgZ63jco6jrAob39qK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1YmD5%2FbtsJXJGEYA9%2FxDyFLgZ63jco6jrAob39qK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;755&quot; height=&quot;239&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[동기 비동기 차이] 이미지 출처 : https://wikidocs.net/228265&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;컴퓨터 자원을 좀 더 효율적으로 사용할 수 있었기 때문에, Reactive Programming을 통해 더 적은 스레드로 더 많은 Request를 처리할 수 있었습니다. 즉 주어진 미션대로 처리량을 높일 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나.. Reactive Programming은 다음과 같은 단점들이 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 높은 진입장벽&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;하나의 스레드가 동시에 두 개 이상의 Request를 처리하게 한다&quot;라는 것은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;기존의 동기적인 관점에서 벗어나 비동기라는 관점에서 프로그래밍&lt;/b&gt;&lt;/span&gt;을 해야 했습니다. 그렇다보니 러닝커브가 높아지게 됐고, 이는 기존에 사용하던 동기적인 방식의 코드와 로직을 전환하는 것에도 부담이 되는 요소로 작용하게 됐습니다. 여러 로직이 맞물릴 때, 하나만 비동기로 바꾸면 되는 게 아니라 전체를 비동기로 바꿔야 하기 때문인 것도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 직관적인 코드 이해의 어려움&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존의 동기적인 관점에서 코드를 작성할 땐, 주어진 비즈니스 요구사항 즉 비즈니스 프로세스가 이 코드에 잘 반영됐는지 파악하는 것이 상대적으로 쉬웠습니다. 또한 새로운 기능을 추가해야 할 때, 기존 코드의 어느 부분에 추가해야 할지 판단하는 것도 어렵지 않았습니다. 하지만 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Reactive Programming에서는 우리의 비즈니스 프로세스가 이 코드에 잘 반영됐는지, 새로운 기능을 추가한다면 어느 부분에 추가해야 하는지를 판단하는 것이 상대적으로 어려웠습니다&lt;/b&gt;&lt;/span&gt;. 1번과 이어지는 얘기지만, 결국 Reactive Programming은 프로그래밍 패러다임, 즉 프로그래밍을 하는 관점 자체를 달리 하는 것이 원인인 것이죠.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 어려워진 디버깅, 예외처리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존에는 Thread per Request, 하나의 스레드가 하나의 Request을 처리하는 컨셉이었기 때문에, 문제가 발생하면 콜스택 등을 통해 디버깅하거나 예외처리를 해주는 것이 상대적으로 쉬웠습니다. 그러나 Reactive Programming은 Request(다른 말로는 task)를 여러 스텝으로 쪼개고 각 스텝들을 파이프라인(다른 말로는 스트림) 형태로 엮어서 여러 스레드에서 처리하게 됩니다. 그렇다보니 콜스택을 통해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;디버깅하거나 예외처리하는 것이 상대적으로 어려워지게 됐습니다&lt;/b&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리하면, Reactive Programming을 통해 분명 처리량을 높일 수 있었습니다. 하지만 그에 반비례해서 개발 생산성이 떨어진다는 리스크가 있었죠. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;Reactive Programming 이거 분명 잘 쓰면 좋아. 근데 잘 쓰기가 너무 어려워!&quot;&lt;/b&gt;&lt;/span&gt;라는 문제점이 점점 부각되기 시작했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 다시 Thread per Request &amp;amp; Thread Pool 이라는 원점으로 돌아옵니다. 주어진 미션은 여전히 &quot;처리량을 높이는 것&quot;입니다. 하지만 여기에 한 스푼 더 얹어서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;기존의 동기적인 코드 흐름을 유지할 순 없을까?&quot;&lt;/b&gt;&lt;/span&gt; 라는 욕심이 들기 시작합니다. 이를 충족시키는 가장 간단한 방법은 역시나 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;스레드 개수 늘리기&quot;&lt;/b&gt;&lt;/span&gt;이지만, 공간적인 제약 때문에 그렇게 할 수 없었죠. (스레드는 메모리에서 2MB정도까지 공간을 차지할 수 있다고 합니다) 근데 여기서, &quot;그러면 스레드를 가볍게 만들면 엄청 많이 만들 수 있는 거 아냐?&quot; 라는 관점이 나오기 시작합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 아이디어를 바탕으로, 스레드를 가볍게, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;경량&quot;&lt;/b&gt;&lt;/span&gt;으로 만들어서&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;서비스의 처리량 향상&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존의 동기적인 코드 흐름 유지&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라는 2개의 목표를 달성하기 위해 본 포스트의 메인 주제인 Virtual Thread가 등장하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. Virtual Thread의 개념과 장점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br62Rj/btsJZQk7Zj2/D42SkY3ftNoGkGpqlFSZkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br62Rj/btsJZQk7Zj2/D42SkY3ftNoGkGpqlFSZkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br62Rj/btsJZQk7Zj2/D42SkY3ftNoGkGpqlFSZkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr62Rj%2FbtsJZQk7Zj2%2FD42SkY3ftNoGkGpqlFSZkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1444&quot; height=&quot;525&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 말씀드렸듯이 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Java가 사용하는 스레딩 모델은 자바 스레드와 커널 스레드의 1 : 1로 매핑 모델입니다. CPU의 실질적인 스케쥴링 대상은 커널 스레드로, 커널 스레드가 CPU를 점유하면 그에 매핑된 자바 스레드가 수행되는 구조로 이해할 수 있습니다. Virtual Thread는 기존의 자바 스레드보다 더 가벼운 스레드를 만들어 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;하나의 자바 스레드에 다수의 Virtual Thread가 매핑시키는 구조로,&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;커널 스레드가 CPU를 점유하면 매핑된 자바 스레드에 mount된 Virtual Thread가 실행되는 개념&lt;/b&gt;&lt;/span&gt;입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;동작 과정도 간단히 살펴보면, 기존에는 자바 스레드가 Blocking되면 매핑된 커널 스레드까지 Blocking되는 구조였습니다. 그러나 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Virtual Thread는 실행 중 Blocking된다면 mount되어 있던 자바 스레드에서 unmount되고, 다른 Virtual Thread가 자바 스레드에 mount되어 실행되는 구조&lt;/b&gt;&lt;/span&gt;입니다. 따라서 실질적으로 CPU를 점유하고 있는 커널 스레드는 Blocking되지 않게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;이러한 특징 덕분에, Virtual Thread 사용 시 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;기존에 사용하던 동기적인 코드 흐름을 그대로 유지&lt;/b&gt;&lt;/span&gt;할 수 있으면서도, 내부적으론 비동기 방식과 비슷하게 동작하게 되어 Non-Blocking 처리가 가능해져 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;처리량을 높일 수 있게 됩니다&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;. 또한 기존 스레드 간의 Context Switching은 커널 레벨에서 발생하지만 Virtual Thread간의 Context Switching은 유저 레벨에서 발생한다는 특징이 있는데요, 이를 통해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Context Switching 비용을 좀 더 아낄 수 있게 된다&lt;/b&gt;&lt;/span&gt;는 장점도 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;구체적인 동작 방식을 통해 좀 더 살펴보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;4. Virtual Thread의 동작 방식 및 원리&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;0. Virtual Thread 주요 구성 요소&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;동작 방식 설명 전 Virtual Thread의 주요 구성 요소들을 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1502&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oy1W2/btsJY7gGhGG/kiylkyQ1v5HR8NVvI0IsSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oy1W2/btsJY7gGhGG/kiylkyQ1v5HR8NVvI0IsSk/img.png&quot; data-alt=&quot;Virtual Thread 주요 구성 요소&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oy1W2/btsJY7gGhGG/kiylkyQ1v5HR8NVvI0IsSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foy1W2%2FbtsJY7gGhGG%2FkiylkyQ1v5HR8NVvI0IsSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1502&quot; height=&quot;334&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Virtual Thread 주요 구성 요소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;1) ForkJoinPool&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Virtual Thread들의 static 멤버(즉 모든 Virtual Thread가 공유)로, Virtual Thread들을 스케쥴링하는 스케쥴러 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;2) Carrier Thread&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Virtual Thread이 mount되어 실행되는 실질적인 자바 스레드이며,&amp;nbsp; &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Carrier Thread들은 저마다 하나씩 WorkQueue라 불리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;work-stealing 방식으로 동작하는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;큐를 가집니다. &lt;/span&gt;논리적인 관점에서 Carrier Thread는 Virtual Thread와 1 : N의 매핑 관계를 가지며, Carrier Thread와 커널 스레드는 1 : 1의 매핑 관계를 가집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #555555; text-align: start;&quot;&gt;※&lt;/span&gt; work-stealing : Carrier Thread들이 자신의 workQueue에 아무 것도 없으면 다른 Carrier Thread의 workQueue에 있는 걸 훔쳐와서 사용하는데, 이를 work-stealing이라고 표현합니다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;3) Continuation&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Virtual Thread가 실행해야 하는 작업과 그 작업에 대한 정보(어디까지 실행했는지 등)을 가지는 객체입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;4) runContinuation&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Continuation을 실질적으로 실행시키는 일종의 람다식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;1. 동작 방식 - Virtual Thread가 Carrier Thread에 mount되는 과정&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1822&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o4t6p/btsJYbRziuj/jUXTdI8fIma0u6hysJrus0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o4t6p/btsJYbRziuj/jUXTdI8fIma0u6hysJrus0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o4t6p/btsJYbRziuj/jUXTdI8fIma0u6hysJrus0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo4t6p%2FbtsJYbRziuj%2FjUXTdI8fIma0u6hysJrus0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1822&quot; height=&quot;620&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;기존 자바 스레드는 start 메서드를 호출하면 start0라는 native 메서드를 거쳐 JNI를 통해 해당 자바 스레드와 매핑될 새로운 커널 스레드를 생성하게 됩니다. 반면 Virtual Thread는 start메서드 호출 시 submitRunContinuation 메서드가 호출되고, 스케쥴러(ForkJoinPool)의 execute 메서드가 호출됩니다. 그러면 스케쥴러가 적당한 Carrier Thread를 하나 골라 runContinuation을 해당 Carrier Thread의 WorkQueue에 넣어주게 됩니다. 이후 Carrier Thread가 본인의 WorkrQueue에 들어있는 runContiation을 뽑아 실행시키면 Virtual Thread와 Carrier Thread가 mount되고 Virtual Thread가 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;2. 동작 방식 - Virtual Thread가 실행 중 Blocking될 때 unmount되는 과정&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1840&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xjdYW/btsJXV9fISF/JiZkghNTKiGXfa0Nn12HSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xjdYW/btsJXV9fISF/JiZkghNTKiGXfa0Nn12HSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xjdYW/btsJXV9fISF/JiZkghNTKiGXfa0Nn12HSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxjdYW%2FbtsJXV9fISF%2FJiZkghNTKiGXfa0Nn12HSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;638&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Virtual Thread가 실행되다가 I/O 요청 등으로 인해 Blocking되면 내부적으로 park메서드가 호출됩니다. park메서드는 doYield라는 native 메서드를 거쳐 JNI를 통해 jvm단에서 memcpy라는 표준 C 라이브러리 함수를 호출하게 됩니다. Virtual Thread가 사용 중이던 스택 프레임을 Heap에다가 복사해두는 기능을 수행하는 것이며, 이를 통해 나중에 Blocking이 끝나고 Virtual Thread가 재개될 때 중단 지점부터 재개하는 것이 가능해집니다. (memcpy 호출 등은 대표적으로 GitHub에 있는 openjdk를 뜯어보면 볼 수 있습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;이런 과정을 거치며 unmount가 진행되고, Carrier Thread는 다른 Virtual Thread와 mount되게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;3. 동작 방식 - Blocking됐던 Virtual Thread가 다시 재개되는 과정&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1862&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clKK8v/btsJYb450Xv/kCKU1YQ4in9YPUOscKo6bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clKK8v/btsJYb450Xv/kCKU1YQ4in9YPUOscKo6bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clKK8v/btsJYb450Xv/kCKU1YQ4in9YPUOscKo6bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclKK8v%2FbtsJYb450Xv%2FkCKU1YQ4in9YPUOscKo6bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1862&quot; height=&quot;540&quot; data-origin-width=&quot;1862&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;Blocking상태가 끝나면 내부적으로 unpark() 메서드가 호출되며, 다시 submitRunContinuation이 호출되면서 Carrier Thread의 WorkQueue에 runContinuation필드가 들어갑니다. 이후 Carrier Thread가 이를 뽑아내서 mount할 때 아까 Heap에 복사해뒀던 스택 프레임을 다시 복구시켜서 중단 지점부터 작업을 재개합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;4. Virtual Thread 동작 방식 최종 정리&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;901&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcHHC4/btsJZuCEdnL/53wgCuxKJcyM8NZcpzOCik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcHHC4/btsJZuCEdnL/53wgCuxKJcyM8NZcpzOCik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcHHC4/btsJZuCEdnL/53wgCuxKJcyM8NZcpzOCik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcHHC4%2FbtsJZuCEdnL%2F53wgCuxKJcyM8NZcpzOCik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;727&quot; height=&quot;447&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread는 Carrier Thread의 WorkQueue에서 work-strealing 방식으로 mount되어 실행됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;mount된 Virtual Thread는 실행이 끝나면 다른 Virtual Thread로 갈아끼워집니다. (Context Switching)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;mount된 Virtual Thread는 실행 중 Blocking되면 현재까지의 실행 정보(스택 프레임)를 Heap에 저장하고 다른 Virtual Thread로 갈아끼워집니다. (Context Switching)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Blocking이 끝난 Virtual Thread는 mount될 때 Heap에 저장해둔 스택 프레임을 불러와 중단 지점부터 다시 작업을 재개합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;5. Virtual Thread 테스트 - Context Switching&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;앞서 살펴본 것처럼 &lt;/span&gt;Virtual Thread 간의 Context Switching은 Virtual Thread의 실행이 끝났을 때 또는 Virtual Thread가 실행되다가 Blocking됐을 때 발생합니다. 그리고 이 과정은 코드 레벨에서 살펴봤듯 스택 프레임을 Heap에 저장하는 과정 등을 거치며 유저 레벨에서 발생됩니다. (참고 : 스택 프레임을 Heap에 복사할 때 사용되는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;memcpy는 &lt;/span&gt;&lt;b&gt;표준 C 라이브러리 함수로 시스템콜이 아닙니다&lt;/b&gt;&lt;/span&gt;)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드 상에선 그렇게 확인했지만,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 Virtual Thread간의 Context Switching이 유저 레벨에서 발생하는지를 직접 검증하고 싶었습니다. 그래서 다음과 같은 코드를 준비했습니다. 코드 상에서 회사명이 나오는 부분들은 가렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;908&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfE7rE/btsJZreTzl6/YijJw2ISS5sjnYTW0oUsL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfE7rE/btsJZreTzl6/YijJw2ISS5sjnYTW0oUsL1/img.png&quot; data-alt=&quot;기존 스레드를 사용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfE7rE/btsJZreTzl6/YijJw2ISS5sjnYTW0oUsL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfE7rE%2FbtsJZreTzl6%2FYijJw2ISS5sjnYTW0oUsL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;455&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존 스레드를 사용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;총 200개(THREAD_COUNT)의 자바 스레드를 만들고, 각각의 스레드가 10ms 동안 대기하는 과정을 100번(ITERATIONS)만큼 반복하여, 프로그램 실행 중 최소 20,000번(THREAD_COUNT X ITERATIONS) 이상의 Context Switching이 발생하도록 합니다. 동일한 코드를 Virtual Thread를 활용한 방식으로도 작성해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;924&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AnaGj/btsJZLxwSfd/1KdJckf35mk57R0h6RPAJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AnaGj/btsJZLxwSfd/1KdJckf35mk57R0h6RPAJK/img.png&quot; data-alt=&quot;Virtual Thread를 사용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AnaGj/btsJZLxwSfd/1KdJckf35mk57R0h6RPAJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAnaGj%2FbtsJZLxwSfd%2F1KdJckf35mk57R0h6RPAJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;675&quot; height=&quot;437&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Virtual Thread를 사용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 각각의 프로그램들을 컴파일한 뒤, Linux에서 제공하는 커널 퍼포먼스 측정 도구인 perf를 활용해 각 프로그램을 실행하는 과정에서 Context Switching이 몇 번 발생했는지 측정해줍니다. 이때 커널 레벨에서 발생한 Context Switching이 측정되는 것이므로, 정말 Virtual Thread가 Context Switching이 유저 레벨에서 발생한다면 기존 스레드를 사용했을 때가 Context Switching 횟수가 더 높게 측정될 것임을 예측할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;총 3번씩 측정했으며, 결과는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;846&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FyJG5/btsJZKSVBYz/RPQceDBQDFfvBmMJbAN3BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FyJG5/btsJZKSVBYz/RPQceDBQDFfvBmMJbAN3BK/img.png&quot; data-alt=&quot;기존 스레드에 대한 측정 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FyJG5/btsJZKSVBYz/RPQceDBQDFfvBmMJbAN3BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFyJG5%2FbtsJZKSVBYz%2FRPQceDBQDFfvBmMJbAN3BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;533&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존 스레드에 대한 측정 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efAgRp/btsJ0u9HacW/kuwrkMTzebEHIeKNFKwQZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efAgRp/btsJ0u9HacW/kuwrkMTzebEHIeKNFKwQZ0/img.png&quot; data-alt=&quot;Virtual Thread에 대한 측정 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efAgRp/btsJ0u9HacW/kuwrkMTzebEHIeKNFKwQZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FefAgRp%2FbtsJ0u9HacW%2FkuwrkMTzebEHIeKNFKwQZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;539&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Virtual Thread에 대한 측정 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.3798%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 34.031%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 스레드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Virtual Thread&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.3798%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Context Switching 횟수&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.031%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;약 19,900 / s&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;약 5,300 / s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.3798%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;유저 모드, 커널 모드 실행 시간 비율&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.031%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;약 1.3 : 1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;약 6.2 : 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 이 테스트에서 기존 스레드는 커널 스레드가 생성되는 과정이 함께 측정되는 부분도 감안해야 합니다. 그러나 Virtual Thread를 사용한 프로그램이 기존 스레드를 사용한 프로그램보다 3배 이상 커널 레벨에서 Context Switching이 덜 발생했고, 유저 모드와 커널 머드에서의 실행 시간 비율을 볼 때도 Virtual Thread를 사용했을 때가 유저 레벨에서 실행된 비율이 더 높음을 쉽게 파악할 수 있습니다. 이를 통해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Virtual Thread간의 Context Switching이 유저 레벨에서 발생한다는 것을 실질적으로 확인&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;할 수 있었고, 따라서 Virtual Thread를 통해 Context Switching 비용을 줄일 수 있음을 도출할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6. Virtual Thread 테스트 - Throughput&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번에는 Virtual Thread 사용시 정말 처리량을 높일 수 있는지를 테스트해봤습니다. 실제 업무에서 쓰이는 API에 테스트해보면 좋겠지만 그럴 수 없던 관계로.. 다음과 같이 Spring Boot를 활용해 간단히 2개의 API를 만들어줬습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1170&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9G12w/btsJY4q3m8o/Y3d2O4yHqh1HhjU3QqqNhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9G12w/btsJY4q3m8o/Y3d2O4yHqh1HhjU3QqqNhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9G12w/btsJY4q3m8o/Y3d2O4yHqh1HhjU3QqqNhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9G12w%2FbtsJY4q3m8o%2FY3d2O4yHqh1HhjU3QqqNhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;408&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;첫 번째로는 500ms씩 2번 Sleep하게 하는 API를 만들어주고, 두 번째로는 1부터 1억까지의 연산을 2번 반복하는 API를 만들어줘서 각각 I/O 작업 위주로 비즈니스 로직을 처리하는 상황과 CPU 연산 위주로 비즈니스 로직을 처리하는 상황을 가정해줍니다. 이를 토대로 I/O Bound 상황과 CPU Bound 상황에 대해, 기존 스레드 사용 시와 Virtual Thread의 사용 시의 처리량을 측정해봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테스트 환경은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Ubuntu 24.04&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2 core CPU / 4GB RAM&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring Boot 3.3.4 / OpenJDK21&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;K6로 VU를 2,000까지 늘리며 진행, 각 3회씩 측정&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;먼저 I/O Bound 상황에 대한 결과입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;537&quot; data-origin-height=&quot;805&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZIqr/btsJZS4hHYh/EB5cUO6LgQI9kJJ7VL5sh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZIqr/btsJZS4hHYh/EB5cUO6LgQI9kJJ7VL5sh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZIqr/btsJZS4hHYh/EB5cUO6LgQI9kJJ7VL5sh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkZIqr%2FbtsJZS4hHYh%2FEB5cUO6LgQI9kJJ7VL5sh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;619&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;805&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;table style=&quot;border-collapse: collapse; width: 100%; height: 154px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;TPS&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Response Time (avg)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Virtual Thread - 1회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;911&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1.06 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Virtual Thread - 2회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;881&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1.07 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Virtual Thread - 3회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;839&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1.08 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 스레드 - 1회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;189&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5.27 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 스레드 - 2회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;188&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5.27 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 스레드 - 3회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;189&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; text-align: center; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5.27 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 간단한 코드를 기반으로 테스트한 것이므로 해당 결과가 실제 운영 환경을 대변하진 않습니다. 그래도 결과를 해석해보면, I/O Bound 상황에서 Virtual Thread는 Non-Blocking 처리가 가능하기 때문에 기존 스레드보다 더 높은 처리량을 보이고 있음을 눈으로 확인해볼 수 있습니다. 또한 Spring Boot에 내장된 톰캣의 스레드풀 사이즈를 별도로 설정해주지 않아 스레드풀 사이즈가 디폴트값인 200으로 만들어졌었는데요. 500ms씩 2번, 총 1초 정도를 대기하도록 API를 구성했기 때문에 기존 스레드를 사용한 결과가 TPS가 200 가까이는 올라가지만 200을 넘지 못하고 있는 것도 확인해볼 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음으론 CPU Bound 상황에 대한 결과입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;554&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blmBOg/btsJ0gcFjFp/PXb9csVxU0nTbUNz7p4FS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blmBOg/btsJ0gcFjFp/PXb9csVxU0nTbUNz7p4FS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blmBOg/btsJ0gcFjFp/PXb9csVxU0nTbUNz7p4FS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblmBOg%2FbtsJ0gcFjFp%2FPXb9csVxU0nTbUNz7p4FS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;397&quot; height=&quot;606&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 154px;&quot; border=&quot;1&quot; data-ke-style=&quot;style8&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;TPS&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Response Time (avg)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Virtual Thread - 1회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;21&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;28.19 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Virtual Thread - 2회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;20&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;28.40 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Virtual Thread - 3회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;20&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;28.38 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 스레드 - 1회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;23&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;27.10 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 스레드 - 2회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;24&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;26.91 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 28.7984%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기존 스레드 - 3회&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.5891%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;23&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.6124%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;27.12 s&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마찬가지로 이 결과가 운영 환경을 대변하진 않지만, 결과를 보면 CPU Bound 상황에서는 Virtual Thread 사용시 오히려 처리량이 낮아지는 걸 볼 수 있습니다. Virtual Thread는 앞서 살펴본 것처럼 실행 시 Carrier Thread의 WorkQueue에 들어갔다가 mount되고, 다 끝난 뒤엔 unmount를 하는 과정 등을 거칩니다. 따라서 작업을 실행하는 데 드는 비용 자체는 기존 스레드가 Virtual Thread보다 더 저렴합니다. Blocking을 처리하는 비용이 Virtual Thread가 기존 스레드보다 훨씬 더 저렴한 것이죠. 따라서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;작업을 실행하는 도중에 Blocking이 발생하지 않는 경우라면 오히려 Virtual Thread를 사용하는 것이 안티 패턴이 될 수도 있음&lt;/b&gt;&lt;/span&gt;을 도출할 수 있습니다. 이 테스트에서 사용된 CPU Bound 상황에 대한 것을 그 예로 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;7.Virtual Thread 주의사항 - Pinning&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread는 Carrier Thread에 mount되어 수행되다가, Blocking되면 Carrier Thread로부터 unmount된다고 소개했습니다. 근데 Virtual Thread가 Carrier Thread에 mount된 상태로 고정되어, Blocking이 발생해도 unmount가 되지 않는 상황이 발생할 수 있습니다. 이를 Pinning현상(Pinned 현상이라고도 부릅니다)이라고 부르며, 다음 상황에서 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread에서 synchronized 블록 사용 시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Virtual Thread에서 native method 사용 시&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;synchronized 블록은 모니터 락을 사용하여 동시 접근을 제어하는데, Virtual Thread가 아닌 Carrier Thread 수준(jvm 단)에서 이 락을 잡다보니 락을 획득한 Carrier Thread가 연관된 작업을 처리하는 동안 다른 작업을 처리할 수 없도록 고정되는 것이 원인입니다. Native method 역시 비슷한 이유로 고정됩니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 Pinning 현상이 발생하면 Virtual Thread의 장점이자 존재 이유(?)인 &quot;Blocking 시 갈아끼우기&quot;를 시전할 수 없게 되므로 성능 저하의 직접적인 원인이 될 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Pinning 현상을 직접 테스트하기 위해 다음과 같은 코드를 준비했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;840&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCQQDt/btsJYHv32xz/xIezNDHsKcx4HPoRujntQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCQQDt/btsJYHv32xz/xIezNDHsKcx4HPoRujntQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCQQDt/btsJYHv32xz/xIezNDHsKcx4HPoRujntQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCQQDt%2FbtsJYHv32xz%2FxIezNDHsKcx4HPoRujntQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;296&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;374&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;794&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vDXth/btsJZUnvnED/5vZktvRfHvcB9dxkUkzlek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vDXth/btsJZUnvnED/5vZktvRfHvcB9dxkUkzlek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vDXth/btsJZUnvnED/5vZktvRfHvcB9dxkUkzlek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvDXth%2FbtsJZUnvnED%2F5vZktvRfHvcB9dxkUkzlek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;233&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;302&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;758&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMHWSq/btsJYcQFOii/US4XDtaTKymbLuvZUM2Yo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMHWSq/btsJYcQFOii/US4XDtaTKymbLuvZUM2Yo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMHWSq/btsJYcQFOii/US4XDtaTKymbLuvZUM2Yo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMHWSq%2FbtsJYcQFOii%2FUS4XDtaTKymbLuvZUM2Yo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;616&quot; height=&quot;257&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2개의 Virtual Thread를 만들어 실행하는데, 각 Virtual Thread는 현재 스레드의 정보를 로그로 남기고, 2초 대기하다가 다시 스레드의 정보를 로그로 남기는 메서드를 실행하도록 합니다. 이 때 run이란 메서드를 synchronized로 묶지 않았을 때와 묶을 때 각각 어떻게 결과가 나오는지를 테스트합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 Carrier Thread는 여러 개가 만들어질 수 있는데, 제가 만든 2개의 Virtual Thread가 서로 다른 Carrier Thread에 mount되어 실행된다면 이 테스트는 의미가 없습니다. 따라서 다음과 같이 옵션을 줘서 Carrier Thread가 하나만 만들어지게 하고 테스트를 진행했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728461508278&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-Djdk.virtualThreadScheduler.parallelism=1 
-Djdk.virtualThreadScheduler.maxPoolSize=1 
-Djdk.virtualThreadScheduler.minRunnable=1&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;962&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GSQdv/btsJ0hbAkTn/0hjKiU0bCZuR6fWryg6oF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GSQdv/btsJ0hbAkTn/0hjKiU0bCZuR6fWryg6oF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GSQdv/btsJ0hbAkTn/0hjKiU0bCZuR6fWryg6oF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGSQdv%2FbtsJ0hbAkTn%2F0hjKiU0bCZuR6fWryg6oF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;962&quot; height=&quot;128&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;테스트 결과는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1106&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mIWaM/btsJ0esoL6y/QzskP4nj9giC2XrzoYrGI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mIWaM/btsJ0esoL6y/QzskP4nj9giC2XrzoYrGI1/img.png&quot; data-alt=&quot;Pinning이 발생하지 않은 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mIWaM/btsJ0esoL6y/QzskP4nj9giC2XrzoYrGI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmIWaM%2FbtsJ0esoL6y%2FQzskP4nj9giC2XrzoYrGI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1106&quot; height=&quot;126&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pinning이 발생하지 않은 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKVSrH/btsJZLxyf0V/OipSdpkKfpPaZ8x2sZSkW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKVSrH/btsJZLxyf0V/OipSdpkKfpPaZ8x2sZSkW0/img.png&quot; data-alt=&quot;Pinning이 발생한 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKVSrH/btsJZLxyf0V/OipSdpkKfpPaZ8x2sZSkW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKVSrH%2FbtsJZLxyf0V%2FOipSdpkKfpPaZ8x2sZSkW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1106&quot; height=&quot;124&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pinning이 발생한 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 로그에 남아있는 스레드 정보가 ForkJoinPool-1-worker-1로 동일하기 때문에, 하나의 Carrier Thread에서 Virtual Thread들이 실행됐음을 볼 수 있습니다. Pinning이 발생하지 않은 경우 VT1이 먼저 로그를 찍고 sleep에 들어가면서 VT2로 갈아끼워지게 되고, VT2도 로그를 찍은 뒤 sleep에 들어가게 된 것을 볼 수가 있습니다. 2초 뒤에 VT1, VT2 둘 다 깨어나면서 로그를 찍게 되므로 전체 실행 시간이 약 2초임을 볼 수 있습니다. 반면 Pinning이 발생한 경우 VT1이 먼저 로그를 찍고 sleep에 들어가지만 VT2로 갈아끼워지지 못하게 되고, 2초 뒤에야 VT2로 갈아끼워지면서 전체 실행 시간이 4초 정도로 나오는 것을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 통해 실질적으로 Virtual Thread 사용 중 Pinning이 발생한다면 성능 저하의 원인이 될 수 있음을 도출했습니다. Pinning이 발생된 구간에서 Blocking이 발생하지 않는다고 해도, 앞서 말씀드린 것처럼 작업을 실행하는 비용 자체는 기존 스레드가 더 저렴하기 때문에 Virtual Thread 사용 시 Pinning이 발생하지 않도록 주의할 필요가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;글을 작성하는 시점인 24년 10월 9일 현재, Pinning에 대해 다음과 같이 조치할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;synchronized block은 ReentrantLock으로 대체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;native method 사용은 최애한 지양&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 현재 여러 Java Library들이 Virtual Thread와의 호환을 위해 내부적으로 사용하던 synchronized를 ReentrantLock으로 바꾸는 작업을 진행하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1068&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EuvGJ/btsJZleNVxm/NfUSLqHkviuDCKOIZH0TQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EuvGJ/btsJZleNVxm/NfUSLqHkviuDCKOIZH0TQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EuvGJ/btsJZleNVxm/NfUSLqHkviuDCKOIZH0TQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEuvGJ%2FbtsJZleNVxm%2FNfUSLqHkviuDCKOIZH0TQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;473&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;8. 정리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 그래서 언제 쓰면 좋을까?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 다시 한 번 상기할 점은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;작업을 실행하는 데 드는 비용 자체는 기존 스레드가 더 저렴하나, Blocking 처리 비용이 Virtual Thread가 훨씬 저렴&lt;/b&gt;&lt;/span&gt;하다는 것입니다. 또한 Virtual Thread는 지연시간이 아닌 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;처리량을 높이는 기술&lt;/b&gt;&lt;/span&gt;이므로, API 응답 시간을 줄이는 것 등이 주어진 미션일 때는 Virtual Thread 도입을 권장하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;현재 시스템이 Thread per Request를 기반으로 하는 Spring mvc 등을 사용 중일 때 Virtual Thread를 도입을 고려&lt;/b&gt;&lt;/span&gt;할 수 있습니다. 만약 현재 시스템이 Reactive Programming의 대표 격인 Spring Webflux 등을 사용 중이라면, Virtual Thread와는 패러다임 자체가 다르므로(Reactive Programming인 비동기, Virtual Thread는 동기) Virtual Thread 도입은 안티 패턴이 될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;현재 시스템이 받는 워크로드가 I/O 작업 위주이며, 시스템의 주 병목 원인이 이 I/O 작업들 위주&lt;/b&gt;&lt;/span&gt;인 것이 분명하다면 Virtual Thread 도입 시 처리량 향상을 꾀할 수 있습니다. 만약 워크로드가 CPU 연산 위주라면, 아까 살펴본 것처럼 Virtual Thread의 도입은 안티 패턴이 될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) Virtual Thread 도입 시 검토해볼 것들&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Pinning 발생을 방지하기 위해, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;사용 중인 코드나 사용 중인 라이브러리 또는 프레임워크 내부에 synchronized 등을 사용하는 곳이 있는지 검토&lt;/b&gt;&lt;/span&gt;해야 합니다. &lt;span style=&quot;color: #242424;&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #242424;&quot;&gt;Djdk.tracePinnedThreads 옵션을 통해 Pinning을 trace할 수 있으며, 특히 Pinning이 발생한 구간에서 Blocking되는 부분이 있는지도 검토해야 합니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 Virtual Thread 사용 시, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;시스템과 연동되는 타 시스템으로 폭발적인 트래픽 전파가 발생할 수 있음을 주의&lt;/b&gt;&lt;/span&gt;해야 합니다. 처리량이 올라가게 되면, 평소에 다른 시스템으로 10개의 요청을 보내다가 갑자기 100개 1,000개 10,000개 이상의 트래픽을 보내게 될 수 있습니다. Virtual Thread에는 배압을 조절하는 기능이 아직 없다보니, 상대 시스템도 이러한 트래픽을 처리할 수 있는 능력(?)이 되는지를 검토해볼 필요가 있습니다. 특히 DB connection같은 한정된 개수의 자원에 접근하는 경우, 내가 보낸 요청들이 되려 timeout이 나면서 떨어질 수 있는 가능성도 있습니다. 현재는 세마포어를 통해 배압을 조절하는 것이 권장되고 있으나, 결국 개발자 본인들이 Virtual Thread를 통해 해당 내용에 대해 인지하고 있을 필요가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 외에도, Virtual Thread는 기존 스레드와 달리 매우 많은 수가 생성될 수 있으므로 ThreadLocal 등을 사용하는 곳이 있는지 검토할 필요가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리하면, Virtual Thread를 실질적으로 도입하게 될 경우&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우리가 사용 중인 라이브러리 등의 대응이 가능한가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우리 서비스가 놓여진 인프라가 뒷밤침될 수 있는가?&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;를 검토해야겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;참고한 레퍼런스&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://junhkang.tistory.com/37&quot;&gt;https://junhkang.tistory.com/37&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@kimyj1234/Java-가상-스레드-Virtual-Thread&quot;&gt;https://velog.io/@kimyj1234/Java-가상-스레드-Virtual-Thread&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kkyu0718.tistory.com/126&quot;&gt;https://kkyu0718.tistory.com/126&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/&quot;&gt;https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/ro-spring-virtual-thread/&quot;&gt;https://tech.kakaopay.com/post/ro-spring-virtual-thread/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://0soo.tistory.com/259&quot;&gt;https://0soo.tistory.com/259&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spring.io/blog/2022/10/11/embracing-virtual-threads&quot;&gt;https://spring.io/blog/2022/10/11/embracing-virtual-threads&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.java/learn/new-features/virtual-threads/&quot;&gt;https://dev.java/learn/new-features/virtual-threads/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dzone.com/articles/the-long-road-to-java-virtual-threads&quot;&gt;https://dzone.com/articles/the-long-road-to-java-virtual-threads&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/1203723&quot;&gt;https://d2.naver.com/helloworld/1203723&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://notypie.dev/java-virtual-thread-와-platform-thread/&quot;&gt;https://notypie.dev/java-virtual-thread-와-platform-thread/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.to/elayachiabdelmajid/java-21-virtual-threads-1h5b&quot;&gt;https://dev.to/elayachiabdelmajid/java-21-virtual-threads-1h5b&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nomoresanta.tistory.com/4&quot;&gt;https://nomoresanta.tistory.com/4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@appti/Virtual-Thread&quot;&gt;https://velog.io/@appti/Virtual-Thread&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.honeybomb.kr/9&quot;&gt;https://blog.honeybomb.kr/9&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/JAVA</category>
      <category>blocking</category>
      <category>java</category>
      <category>jdk21</category>
      <category>Pinned</category>
      <category>pinning</category>
      <category>Virtual Thread</category>
      <category>가상 스레드</category>
      <category>논블로킹</category>
      <category>자바</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/139</guid>
      <comments>https://jofestudio.tistory.com/139#entry139comment</comments>
      <pubDate>Wed, 9 Oct 2024 17:57:05 +0900</pubDate>
    </item>
    <item>
      <title>JNI(Java Native Interface)란? (feat. 자바 스레드 생성)</title>
      <link>https://jofestudio.tistory.com/138</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JNI란?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Java Native Interface의 약자로, 자바 코드에서 네이티브 코드(하드웨어와 운영체제가 직접 실행할 수 있는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기계어&lt;/b&gt;&lt;/span&gt; 또는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;바이너리 코드&lt;/b&gt;&lt;/span&gt;를 의미)를 호출하거나, 반대로 네이티브 코드에서 Java 코드를 호출할 수 있게 해주는 프레임워크를 말합니다. C언어 또는 C++언어로 작성된 코드는 컴파일 시 해당 하드웨어에서 직접 구동될 수 있는 기계어로 컴파일되기 때문에 네이티브 코드에 해당되는데요(자바는 플랫폼에 독립적인 바이트코드로 컴파일된다는 점을 참고하면 좋습니다). 따라서 자바 코드에서 JNI를 통해 C언어 또는 C++언어로 작성된 코드를 실행할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주로 다음과 같은 상황들에 JNI를 활용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성능 최적화&lt;/span&gt; :&lt;/b&gt; 성능이 중요한 부분을 C/C++로 구현하고, 이를 자바 코드에서 호출할 때 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 라이브러리 사용&lt;/span&gt; :&lt;/b&gt; 이미 C/C++로 작성된 기존 라이브러리나 API를 자바 애플리케이션에서 활용하고자 할 때 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;플랫폼 종속 기능&lt;/span&gt; :&lt;/b&gt; 플랫폼에 특화된 기능(시스템콜 호출, 하드웨어 제어 등)이 필요한 경우 사용&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용 방법&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메서드 앞에 native를 붙임으로써 네이티브 메서드임을 명시할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726315568980&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Jofe {
    public native void nativeMethod();

    static {
        System.loadLibrary(&quot;jofe&quot;);
    }

    public static void main(String[] args) {
        new Jofe().nativeMethod();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;native로 선언된 nativeMethod는 해당 메서드가 네이티브 라이브러리에서 구현됨을 의미합니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Jofe클래스가 처음 로드될 때 네이티브 라이브러리인 jofe를 로드하는 것도 확인 가능합니다. main 메서드에서 Jofe 객체를 생성하고 nativeMethod를 호출하면, JVM은 네이티브 코드로 연결되어 해당 네이티브 메서드가 실행되게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음 명령어의 실행을 통해 JNI를 사용하기 위한 헤더 파일을 만들 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726332869634&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;javac -h {헤더파일을 둘 위치} {Jofe.java의 경로}
# ex : javac -h . Jofe.java&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러면 다음과 같이 헤더파일이 만들어집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726332919581&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* DO NOT EDIT THIS FILE - it is machine generated */
#include &amp;lt;jni.h&amp;gt;
/* Header for class Jofe */

#ifndef _Included_Jofe
#define _Included_Jofe
#ifdef __cplusplus
extern &quot;C&quot; {
#endif
/*
 * Class:     Jofe
 * Method:    nativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Jofe_nativeMethod
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 헤더파일을 기반으로, c언어로 해당 네이티브 메서드를 다음과 같이 구현할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726333039036&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;jni.h&amp;gt;
#include &quot;Jofe.h&quot;

JNIEXPORT void JNICALL Java_Jofe_nativeMethod(JNIEnv *env, jobject obj) {
       printf(&quot;이거 하나 출력할라고 개고생중입니다&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 c파일을 컴파일하여 다음과 같이 라이브러리를 만들 수 있습니다. 저는 macos여서 다음 커맨드를 사용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726333282331&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -shared -m64 -o {생성할 라이브러리 이름} {c파일 경로}
# ex : gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -shared -m64 -o libjofe.dylib Jofe.c&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 컴파일된 Jofe.class를 다음과 같이 네이티브 라이브러리의 경로를 지정해서 실행해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726366065583&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -Djava.library.path={생성한 라이브러리 위치} -classpath {컴파일된 클래스파일위치} {실행할 클래스명}
# java -Djava.library.path=/Users/jofejofe/Development/JNI/src/main/java/org/example -classpath . Jofe&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러면 c파일에서 구현된 메서드가 실행되는 걸 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실제로 자바에선 JNI가 어떻게 활용되고 있을까?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;대표적으로 스레드의 생성 등에 활용하고 있습니다. 다음과 같이 Thread의 start()메서드를 까보면, 내부적으로 start0()이란 네이티브 메서드를 호출하고 있음을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726368593724&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // Thread.java
    
    public void start() {
        synchronized (this) {
            // zero status corresponds to state &quot;NEW&quot;.
            if (holder.threadStatus != 0)
                throw new IllegalThreadStateException();
            start0();
        }
    }
    
    private native void start0();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 메서드의 구현체는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/blob/221e1a426070088b819ddc37b7ca77d9d8626eb4/src/java.base/share/native/libjava/Thread.c&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/openjdk/jdk/blob/221e1a426070088b819ddc37b7ca77d9d8626eb4/src/java.base/share/native/libjava/Thread.c&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726368812037&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;jdk/src/java.base/share/native/libjava/Thread.c at 221e1a426070088b819ddc37b7ca77d9d8626eb4 &amp;middot; openjdk/jdk&quot; data-og-description=&quot;JDK main-line development https://openjdk.org/projects/jdk - openjdk/jdk&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/openjdk/jdk/blob/221e1a426070088b819ddc37b7ca77d9d8626eb4/src/java.base/share/native/libjava/Thread.c&quot; data-og-url=&quot;https://github.com/openjdk/jdk/blob/221e1a426070088b819ddc37b7ca77d9d8626eb4/src/java.base/share/native/libjava/Thread.c&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/coVyc5/hyW2WMNW6l/iNKUY3Wi6wvYpJ8CTpWq71/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/blob/221e1a426070088b819ddc37b7ca77d9d8626eb4/src/java.base/share/native/libjava/Thread.c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/openjdk/jdk/blob/221e1a426070088b819ddc37b7ca77d9d8626eb4/src/java.base/share/native/libjava/Thread.c&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/coVyc5/hyW2WMNW6l/iNKUY3Wi6wvYpJ8CTpWq71/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;jdk/src/java.base/share/native/libjava/Thread.c at 221e1a426070088b819ddc37b7ca77d9d8626eb4 &amp;middot; openjdk/jdk&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JDK main-line development https://openjdk.org/projects/jdk - openjdk/jdk&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1726368852132&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;jni.h&quot;
#include &quot;jvm.h&quot;

#include &quot;java_lang_Thread.h&quot;

#define THD &quot;Ljava/lang/Thread;&quot;
#define OBJ &quot;Ljava/lang/Object;&quot;
#define STE &quot;Ljava/lang/StackTraceElement;&quot;
#define STR &quot;Ljava/lang/String;&quot;

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

static JNINativeMethod methods[] = {
    {&quot;start0&quot;,           &quot;()V&quot;,        (void *)&amp;amp;JVM_StartThread},
    {&quot;isAlive0&quot;,         &quot;()Z&quot;,        (void *)&amp;amp;JVM_IsThreadAlive},
    {&quot;setPriority0&quot;,     &quot;(I)V&quot;,       (void *)&amp;amp;JVM_SetThreadPriority},
    {&quot;yield0&quot;,           &quot;()V&quot;,        (void *)&amp;amp;JVM_Yield},
    {&quot;sleep0&quot;,           &quot;(J)V&quot;,       (void *)&amp;amp;JVM_Sleep},
    {&quot;currentCarrierThread&quot;, &quot;()&quot; THD, (void *)&amp;amp;JVM_CurrentCarrierThread},
    {&quot;currentThread&quot;,    &quot;()&quot; THD,     (void *)&amp;amp;JVM_CurrentThread},
    // ...생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;네이티브 메서드의 이름과 c로 작성된 함수의 포인터를 매핑하고 있는 걸 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JVM_StartThread는 다음과 같이 구현되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726369521815&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// jobject : 여기선 Java 스레드 객체(java.lang.Thread)를 나타냄. 이를 통해 스레드를 시작
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
#if INCLUDE_CDS
  // .. 생략
#endif
  // JVM 내에서 자바스레드(유저스레드)를 표현하고 관리하는 핵심 클래스
  // 자바스레드(유저스레드)와 커널 스레드를 연결하는 역할
  JavaThread *native_thread = NULL;

  // 이미 시작된 스레드를 다시 시작하려고 하는 경우 예외를 던지는지 여부를 결정하는 플래그
  bool throw_illegal_thread_state = false;

  {
    // 뮤텍스 잠그고 시작
    MutexLocker mu(Threads_lock);

    // 해당 Java 스레드 객체가 이미 시작된 스레드라면 =&amp;gt; 플래그를 설정
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      // 스레드가 이미 시작되지 않은 경우, 스택 크기를 가져와서 새로운 JavaThread 객체 생성
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      // 
      NOT_LP64(if (size &amp;gt; SIZE_MAX) size = SIZE_MAX;)
      size_t sz = size &amp;gt; 0 ? (size_t) size : 0;
      // 여기서 커널스레드가 새로 만들어짐
      native_thread = new JavaThread(&amp;amp;thread_entry, sz);

      // 커널스레드가 만들어졌다면, 자바스레드(유저스레드)를 매핑
      if (native_thread-&amp;gt;osthread() != NULL) {
        native_thread-&amp;gt;prepare(jthread);
      }
    }
  }
  
  // 플래그 설정에 따른 예외 던지기
  if (throw_illegal_thread_state) {
    THROW(vmSymbols::java_lang_IllegalThreadStateException());
  }

  assert(native_thread != NULL, &quot;Starting null thread?&quot;);
   
  // 일종의 예외 처리
  if (native_thread-&amp;gt;osthread() == NULL) {
    ResourceMark rm(thread);
    log_warning(os, thread)(&quot;Failed to start the native thread for java.lang.Thread \&quot;%s\&quot;&quot;,
                            JavaThread::name_for(JNIHandles::resolve_non_null(jthread)));
 
    native_thread-&amp;gt;smr_delete();
    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
        os::native_thread_creation_failed_msg());
    }
    THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
              os::native_thread_creation_failed_msg());
  }

  JFR_ONLY(Jfr::on_java_thread_start(thread, native_thread);)
  
  // 스레드 시작
  Thread::start(native_thread);

JVM_END&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주석에도 명시했지만, new JavaThread()에서 다음과 같이 커널스레드가 생성되는 과정도 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726410757898&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : JavaThread() {
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &amp;amp;CompilerThread::thread_entry ? os::compiler_thread :
                                                            os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이와 같이.. 자바 코드에서 시스템콜이 필요해지는 경우, 개발자가 직접 사용하든 안 하든 JNI를 활용해 네이티브 코드를 호출하여 시스템콜을 수행하게 됩니다. System.out.println도 다음과 같이 JNI를 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726411548525&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private native void write(int b, boolean append) throws IOException;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/JAVA</category>
      <category>java</category>
      <category>Java Native Interface</category>
      <category>JNI</category>
      <category>system call</category>
      <category>시스템콜</category>
      <category>자바</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/138</guid>
      <comments>https://jofestudio.tistory.com/138#entry138comment</comments>
      <pubDate>Sun, 15 Sep 2024 23:49:56 +0900</pubDate>
    </item>
    <item>
      <title>이 함수형 인터페이스에는 추상메서드가 2개 이상인데요? (feat.Object)</title>
      <link>https://jofestudio.tistory.com/137</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;들어가며&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자바의 함수형 인터페이스(Functional Interface)는 람다식과의 호환성을 위해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;하나의 추상 메서드&lt;/b&gt;&lt;/span&gt;만 가져야 합니다. 람다식은 실제론 &quot;익명 클래스(Anonymous Class)의 객체&quot;와 동등하기 때문에, 람다식이 함수형 인터페이스에 작성된 메서드와 1 : 1로 매핑되기 위해서 함수형 인터페이스는 하나의 추상 메서드만 가져야 한다는 것이죠. 실제로 다음과 같이 함수형 인터페이스에 두 개 이상의 추상 메서드를 작성하면 오류가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;956&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UG3mj/btsIHBiGBjX/xS8gjLofGnfv2d8z2lawA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UG3mj/btsIHBiGBjX/xS8gjLofGnfv2d8z2lawA1/img.png&quot; data-alt=&quot;함수형 인터페이스에 2개 이상의 추상 메서드를 작성한 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UG3mj/btsIHBiGBjX/xS8gjLofGnfv2d8z2lawA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUG3mj%2FbtsIHBiGBjX%2FxS8gjLofGnfv2d8z2lawA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;303&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;함수형 인터페이스에 2개 이상의 추상 메서드를 작성한 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나, 자바에서 함수형 인터페이스로 정의된 Comparator는 다음과 같이 두 개의 추상 메서드를 가지고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721550714627&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Comparator&amp;lt;T&amp;gt; {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    // ..생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;equals가 추상메서드로 취급되지 않는 모습을 볼 수 있었던 거죠. 뭔가 해서 직접 실험해보니, 함수형 인터페이스를 작성할 때 다음과 같이 equals를 추상메서드로 작성해도 오류가 나지 않는 걸 볼 수 있었습니다. equals는 Object 클래스에 정의된 메서드로, 마찬가지로 toString같은 Object클래스에 정의된 함수를 함수형 인터페이스에서 추상메서드로 작성해도 오류가 없었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1036&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebd887/btsIIYxeqgr/zJf9rnMtPU5brqhk1N7QK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebd887/btsIIYxeqgr/zJf9rnMtPU5brqhk1N7QK0/img.png&quot; data-alt=&quot;함수형 인터페이스인데, 추상메서드를 3개 작성했음에도 오류가 나지 않는다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebd887/btsIIYxeqgr/zJf9rnMtPU5brqhk1N7QK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Febd887%2FbtsIIYxeqgr%2FzJf9rnMtPU5brqhk1N7QK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;265&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;함수형 인터페이스인데, 추상메서드를 3개 작성했음에도 오류가 나지 않는다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;함수형 인터페이스는 하나의 추상메서드만 가져야 한다는데, 위에서 분명 저는 3개의 추상메서드를 함수형 인터페이스에 작성했음에도 괜찮았습니다. 왜 이런 결과가 나오는지를 조사해봤고, 이 글에서 소개하려 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Object 클래스의 메서드이기 때문이다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이유는 간단하게, equals 등의 메서드는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Object 클래스에 작성된 메서드&lt;/b&gt;&lt;/span&gt;이기 때문입니다. 그리고 자바의 모든 객체는 Object 클래스를 상속(extends)하죠. 그렇기 때문에 괜찮았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;조금 다른 얘기를 먼저 해보겠습니다. 자바에서는 다중 상속을 기본적으로 지원하지 않습니다. A라는 클래스와 B라는 클래스를 상속받을 때 A와 B 둘다 같은 이름의 메서드를 가진다면 모호성이 생기기 때문이죠. 하지만 인터페이스라면, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;어차피 구현을 개발자가 원하는대로 할 수 있기 때문에&amp;nbsp;&lt;/span&gt;여러 인터페이스를 구현할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 참고하면서, 다음 인터페이스를 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721551931830&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface MyInterface {
    void someMethod1();
    void someMethod2();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MyInterface를 구현하는 클래스들은 someMethod1과 someMethod2를 둘 다 구현해야 합니다. 이 상황에서, YourInterface라는 인터페이스와 이를 구현한 YourClass라는 클래스를 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721552105078&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface YourInterface {
    void someMethod2();
}

class YourClass implements YourInterface {
    public void someMethod2() {
        System.out.println(&quot;someMethod2&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;YourInterface는 MyInterface와 마찬가지로 someMethod2라는 이름의 추상 메서드를 가지며, YourClass가 YourInterface를 구현한 모습입니다. 이때 다음과 같이 MyInterface를 구현하는 MyClass라는 클래스를 만들되, MyClass가 YourClass를 상속받게 하면서 someMethod1만 오버라이딩하게 한다면 어떨까요?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1524&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/delZqD/btsIG3NvqmW/z1y67cmqRkOs6onvcNHBvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/delZqD/btsIG3NvqmW/z1y67cmqRkOs6onvcNHBvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/delZqD/btsIG3NvqmW/z1y67cmqRkOs6onvcNHBvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdelZqD%2FbtsIG3NvqmW%2Fz1y67cmqRkOs6onvcNHBvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;203&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;분명 MyClass는 MyInterface를 구현하면서 someMethod2라는 추상 메서드도 오버라이딩해야 하지만, 오류가 나지 않습니다. 왜냐하면 MyClass가 상속하는 YourClass에서 이미 someMethod2라는 메서드를 오버라이딩했기 때문이죠. 따라서 MyClass의 인스턴스를 만들어서 someMethod2를 호출하면, YourClass에서 오버라이딩된 메서드가 호출되게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721552465882&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.someMethod2();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;782&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nfDrC/btsIIiXfunh/M5Cd1G5ei1qxDm2MBtB9rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nfDrC/btsIIiXfunh/M5Cd1G5ei1qxDm2MBtB9rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nfDrC/btsIIiXfunh/M5Cd1G5ei1qxDm2MBtB9rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnfDrC%2FbtsIIiXfunh%2FM5Cd1G5ei1qxDm2MBtB9rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;433&quot; height=&quot;155&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때, MyClass에서 별도로 someMethod2를 오버라이딩하면, MyClass의 인스턴스에서 someMethod2 실행시 오버라이딩된 메서드가 실행되게 되겠죠. (실행결과는 생략..)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 힌트를 얻을 수 있었습니다. 자바에서 모든 클래스는 Object를 상속받습니다. 즉 별도로 구현하지 않아도, equals 등의 메서드를 가지고 있습니다. 따라서 Object 클래스에 정의된 메서드를 추상메서드로 작성해서 인터페이스를 만든 경우, 해당 인터페이스를 구현하는 클래스에서 별도로 그 메서드를 오버라이딩하지 않아도 문제가 생기지 않는 것이죠.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721557337163&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface MyInterface {
    void myMethod();
    boolean equals(Object obj);
}


// Object로부터 상속받은 equals를 가지고 있다
class MyClass implements MyInterface {
    public void myMethod() {
        System.out.println(&quot;MyClass&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서 equals는 MyInterface에서 추상메서드로 작성되긴 했으나, MyInterface에게 본질적인 추상메서드는 1개(myMethod)인 셈입니다. 따라서 함수형 인터페이스를 만들 때 Object 클래스에서 정의된 메서드를 추상메서드로 작성해도, 본질적인 추상메서드만 1개라면 오류가 발생하지 않는 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇다면, 인터페이스에 Object클래스에 정의된 메서드를 추상메서드로 작성하는 건 어떤 의미를 가질까요? 바로 개발자들로 하여금, 해당 인터페이스를 구현하는 클래스를 만들 때 필요하다면 해당 메서드를 오버라이딩해 작성해야 함을 알려주는 기능과, 이 인터페이스를 구현하는 클래스들이 반드시 이 메서드들을 올바르게 구현해야 한다는 의도를 명확히 전달하는 기능을 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>WEB/JAVA</category>
      <category>functionalinterface</category>
      <category>lambda</category>
      <category>object</category>
      <category>람다</category>
      <category>추상메서드</category>
      <category>함수형인터페이스</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/137</guid>
      <comments>https://jofestudio.tistory.com/137#entry137comment</comments>
      <pubDate>Sun, 21 Jul 2024 19:38:24 +0900</pubDate>
    </item>
    <item>
      <title>커널 스레드 vs 유저 스레드(사용자 스레드)</title>
      <link>https://jofestudio.tistory.com/136</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;스레드란&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스레드란 하나하나의 실행 단위를 말하며, 흔히 프로세스를 차를 만드는 공장에 비유한다면 스레드를 바퀴를 만드는 일꾼, 모터를 만드는 일꾼 등에 비유하곤 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1012&quot; data-origin-height=&quot;1104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqCaSQ/btsH88uJ2kM/KW9vbZXwkvc1iM8CidvaDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqCaSQ/btsH88uJ2kM/KW9vbZXwkvc1iM8CidvaDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqCaSQ/btsH88uJ2kM/KW9vbZXwkvc1iM8CidvaDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqCaSQ%2FbtsH88uJ2kM%2FKW9vbZXwkvc1iM8CidvaDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;543&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;1104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; 이 스레드는  커널 공간에서 구현하는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;커널 스레드&lt;/b&gt;&lt;/span&gt;와 유저 공간에서 구현하는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;유저 스레드&lt;/b&gt;&lt;/span&gt; 두 종류로 나눌 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;커널 스레드 (Kernel Threads)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 운영 체제의 &quot;커널&quot;이 관리하는 스레드입니다. 프로세스가 생성되면 커널은 하나의 커널 스레드를 만드는데, &quot;프로세스&quot;가 아닌 이 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;커널 스레드&quot;가 실질적인 스케쥴링 대상&lt;/b&gt;&lt;/span&gt;이 되며 하나의 프로세스는 하나 이상의 커널 스레드를 가집니다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;유저 스레드 (Kernel Threads)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 유저 공간에서 라이브러리에 의해 관리되는 스레드입니다. 커널은 유저 스레드의 존재를 모르며, 모든 유저 스레드의 관리는 유저 공간에서 이루어집니다. 유저 스레드의 실질적인 수행은 자신이 속한 프로세스의 커널 스레드와 연결돼서 수행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;좀 더 이해하기 쉽도록, A라는 프로세스에 대해 커널 스레드를 2개 만들 때와 유저 스레드를 2개 만들 때를 도식화해서 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;748&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGnc2/btsH8T5E2nI/1uyYNEQAEz1VkeWizvVX00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGnc2/btsH8T5E2nI/1uyYNEQAEz1VkeWizvVX00/img.png&quot; data-alt=&quot;스레드를 커널 스레드로 해서 2개 만든 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGnc2/btsH8T5E2nI/1uyYNEQAEz1VkeWizvVX00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGnc2%2FbtsH8T5E2nI%2F1uyYNEQAEz1VkeWizvVX00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;346&quot; height=&quot;485&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스레드를 커널 스레드로 해서 2개 만든 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;1042&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byYqH0/btsH8GyJGby/dXgp9YrZ8K1kEVX8wcXWs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byYqH0/btsH8GyJGby/dXgp9YrZ8K1kEVX8wcXWs0/img.png&quot; data-alt=&quot;스레드를 유저 스레드로 해서 2개 만든 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byYqH0/btsH8GyJGby/dXgp9YrZ8K1kEVX8wcXWs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyYqH0%2FbtsH8GyJGby%2FdXgp9YrZ8K1kEVX8wcXWs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;338&quot; height=&quot;473&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;1042&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스레드를 유저 스레드로 해서 2개 만든 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;실질적으로 스케쥴링 대상이 되는 것은 커널 스레드이며, 커널은 유저 스레드의 존재를 모른다&lt;/b&gt;&lt;/span&gt;고 했습니다.&amp;nbsp; 따라서 프로세스 A, B가 있는 상황에서 A에 커널 스레드만 2개 있는 경우와 A에 유저 스레드가 2개 있는 경우는 다음과 같이 스케쥴링됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1810&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZolIg/btsIajvjsjd/klG7jKWL9gQYz3aXkGoa10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZolIg/btsIajvjsjd/klG7jKWL9gQYz3aXkGoa10/img.png&quot; data-alt=&quot;프로세스 A에 커널 스레드가 2개인 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZolIg/btsIajvjsjd/klG7jKWL9gQYz3aXkGoa10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZolIg%2FbtsIajvjsjd%2FklG7jKWL9gQYz3aXkGoa10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;266&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로세스 A에 커널 스레드가 2개인 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1450&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdD1gd/btsH8iSGUZz/E1PoCh6y3OaoeUgDvrVOi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdD1gd/btsH8iSGUZz/E1PoCh6y3OaoeUgDvrVOi1/img.png&quot; data-alt=&quot;프로세스 A에 유저 스레드가 2개인 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdD1gd/btsH8iSGUZz/E1PoCh6y3OaoeUgDvrVOi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdD1gd%2FbtsH8iSGUZz%2FE1PoCh6y3OaoeUgDvrVOi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;127&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로세스 A에 유저 스레드가 2개인 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로세스 A에 커널 스레드만 2개인 경우, A의 2개 커널 스레드 각각이 스케쥴링 대상이 됩니다. 따라서 A의 0번 스레드와 1번 스레드, 프로세스 B(얘도 B의 커널 스레드라고 볼 수 있겠죠)가 스케쥴링되는 걸 볼 수 있습니다. 그러나 프로세스 A에 유저 스레드가 2개인 경우, 커널은 유저 스레드를 모르며 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;오직 커널 스레드만이 스케쥴링 대상&lt;/b&gt;&lt;/span&gt;이 되기 때문에 A의 커널 스레드와 B의 커널 스레드만이 스케쥴링 대상이 됩니다. 이 때 A의 유저 스레드가 2개인 상황에서 A의 커널 스레드가 실행될 때, 0번 스레드(유저 스레드임)와 1번 스레드(유저 스레드임)를 어떻게 배분할지는 스레드 라이브러리가 책임집니다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;즉 유저 스레드는 자신이 속한 프로세스의 커널 스레드가 CPU에 올라왔을 때, 그 커널 스레드와 연결돼서 수행되는 스레드라고 볼 수 있는 것이죠.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;커널 스레드가 실질적인 스케쥴링 대상이다&quot;&lt;/b&gt;&lt;/span&gt;로 비롯되는 다른 차이점은 어떤 것들이 있을까요? 우선 커널 스레드의 경우 같은 프로세스의 커널 스레드라 할지라도 다른 프로세서에 할당할 수 있는 반면, 같은 프로세스의 유저 스레드들은 다른 프로세서로 할당될 수 없을 수도 있습니다(커널 스레드가 CPU에 올라갔을 때 연결돼서 수행되기 때문). 즉 커널 스레드는 멀티프로세서 시스템에서 유저 스레드보다 더 효율적으로 작동한다고 볼 수 있죠. 그리고 커널 스레드가 시스템콜을 호출하면 해당 스레드만 블로킹되고, 다른 커널 스레드들은 계속 실행될 수 있습니다. 반면 유저 스레드는 하나의 유저 스레드가 시스템콜을 호출하면 같은 프로세스의 다른 유저 스레드들도 블로킹될 수 있습니다. 커널은 그 유저 스레드에 연결된 커널 스레드가 시스템콜을 호출한 거라고 여기기 때문에, 해당 커널 스레드가 블로킹되면서 물려있던다른 유저 스레드들도 블로킹될 여지가 있다는 거죠. 컨텍스트 스위칭 비용의 경우, 커널 스레드의 컨텍스트 스위칭이 유저 스레드의 컨텍스트 스위칭보다 가볍습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;표로 정리하면 다음과 같겠습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;커널 스레드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;유저 스레드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;관리 주체&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커널&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;유저 수준 라이브러리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;생성 및 관리 비용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;상대적으로 높음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;상대적으로 낮음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;블로킹 시스템콜&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한 스레드가 블로킹되어도 다른 스레드에 영향 없음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한 스레드가 블로킹되면 전체가 블로킹될 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;멀티프로세서 지원&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;효율적&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;비효율적&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;컨텍스트 스위칭 비용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;상대적으로 높음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;상대적으로 낮음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼 이제 유저 스레드가 커널 스레드와 연결되는 3가지 스레드 모델을 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. One-to-One Model&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;494&quot; data-origin-height=&quot;187&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djWdYu/btsH8f9vXDe/37BsbK8j46ecsZe0K7PTT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djWdYu/btsH8f9vXDe/37BsbK8j46ecsZe0K7PTT0/img.png&quot; data-alt=&quot;One-to-One Model&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djWdYu/btsH8f9vXDe/37BsbK8j46ecsZe0K7PTT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjWdYu%2FbtsH8f9vXDe%2F37BsbK8j46ecsZe0K7PTT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;465&quot; height=&quot;176&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;187&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;One-to-One Model&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 유저 스레드가 하나의 커널 스레드와 연결되는 모델입니다. 각 커널 스레드는 독립적으로 스케쥴링되므로, 같은 프로세스 내의 유저 스레드들은 각각 다른 프로세서에서 병렬로 실행될 수 있으며 하나의 유저 스레드에서 시스템콜을 호출해도 다른 유저 스레드가 블로킹되지 않습니다. 그러나 유저 스레드의 컨텍스트 스위칭 이점을 볼 수 없는 모델이기도 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2.  Many-to-One Model&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;403&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ef8973/btsH9VO0da9/tTnxFCt5vWpVAknYKX9yJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ef8973/btsH9VO0da9/tTnxFCt5vWpVAknYKX9yJ1/img.png&quot; data-alt=&quot;Many-To-One Model&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ef8973/btsH9VO0da9/tTnxFCt5vWpVAknYKX9yJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fef8973%2FbtsH9VO0da9%2FtTnxFCt5vWpVAknYKX9yJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;403&quot; height=&quot;398&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Many-To-One Model&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여러 유저 스레드가 하나의 커널 스레드와 연결되는 모델입니다. 따라서 같은 프로세스의 유저 스레드들이 서로 다른 프로세서에서 병렬로 실행될 수 없으며, 하나의 유저 스레드에서 시스템콜을 호출한 경우 다른 유저 스레드들도 블로킹될 수 있습니다. 그러나 유저 스레드의 컨텍스트 스위칭 이점을 얻을 수 있는 모델입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3.  Many-to-Many Model&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;469&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTvOSN/btsH8x28AB4/fWQ56gK2zudk7u5YEHECHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTvOSN/btsH8x28AB4/fWQ56gK2zudk7u5YEHECHK/img.png&quot; data-alt=&quot;Many-To-Many Model&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTvOSN/btsH8x28AB4/fWQ56gK2zudk7u5YEHECHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTvOSN%2FbtsH8x28AB4%2FfWQ56gK2zudk7u5YEHECHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;469&quot; height=&quot;399&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Many-To-Many Model&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여러 유저 스레드가 여러 커널 스레드와 연결되는 모델로 One-to-One과 Many-to-One을 짬뽕한 모델입니다. 같은 프로세스의 유저 스레드가 여러 프로세서에서 병렬로 실행될 수 있으며, 한 유저 스레드가 시스템콜을 호출해도 다른 커널 스레드에 연결된 유저 스레드들이 있으니 유저 스레드들이 모두 블로킹되는 불상사를 막을 수 있습니다. 유저 스레드의 컨텍스트 스위칭 이점도 챙길 수 있으나, 설계와 구현이 매우 복잡하며 유저 스레드와 커널 스레드 간 연결 관계의 관리 등에 오버헤드가 들어감을 감안해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 자바에서 21버전부터 Virtual Thread가 도입됐는데요. 원래는 자바에서 유저 스레드를 만들면 그에 연결되는 커널 스레드도 함께 만들어졌습니다. 즉 기존에는 One-to-One 모델을 사용했던 것이죠. 그러나 Virtual Thread는 Many-to-One과 비슷한 컨셉을 차용해서 컨텍스트 스위칭 비용을 낮췄습니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;관심있는 분은 다음 글을 보면 될 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/15398/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/15398/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719122270437&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Java의 미래, Virtual Thread | 우아한형제들 기술블로그&quot; data-og-description=&quot;JDK21에 공식 feature로 추가된 Virtual Thread에 대해 알아보고, Thread, Reactive Programming, Kotlin coroutines와 비교해봅니다.&quot; data-og-host=&quot;techblog.woowahan.com&quot; data-og-source-url=&quot;https://techblog.woowahan.com/15398/&quot; data-og-url=&quot;https://techblog.woowahan.com/15398/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwbGDS/hyWoEGA7ki/aUf4vFdKpDuOhHpPoaXF1K/img.png?width=4000&amp;amp;height=2088&amp;amp;face=0_0_4000_2088,https://scrap.kakaocdn.net/dn/b9Ac2h/hyWrUnrRSB/NrZHpWzxK7f53S6Kupw9KK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/15398/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://techblog.woowahan.com/15398/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwbGDS/hyWoEGA7ki/aUf4vFdKpDuOhHpPoaXF1K/img.png?width=4000&amp;amp;height=2088&amp;amp;face=0_0_4000_2088,https://scrap.kakaocdn.net/dn/b9Ac2h/hyWrUnrRSB/NrZHpWzxK7f53S6Kupw9KK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Java의 미래, Virtual Thread | 우아한형제들 기술블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JDK21에 공식 feature로 추가된 Virtual Thread에 대해 알아보고, Thread, Reactive Programming, Kotlin coroutines와 비교해봅니다.&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS/운영체제</category>
      <category>OS</category>
      <category>Virtual Thread</category>
      <category>운영체제</category>
      <category>유저 스레드</category>
      <category>커널</category>
      <category>커널 스레드</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/136</guid>
      <comments>https://jofestudio.tistory.com/136#entry136comment</comments>
      <pubDate>Sun, 23 Jun 2024 15:02:11 +0900</pubDate>
    </item>
    <item>
      <title>피터슨의 알고리즘</title>
      <link>https://jofestudio.tistory.com/134</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;피터슨의 알고리즘&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;컴퓨터 시스템에서 여러 프로세스가 동시에 실행될 때, 공유 자원에 대해 여러 프로세스에서 동시 접근이 가능하다면 데이터 불일치 등의 문제가 발생할 수 있습니다. 이 때 공유 자원에 대해 동시 접근이 발생 가능한 영역을 임계 영역(Critical section)이라 부르며, 이 임계 영역에서 비롯되는 문제들을 막으려면 다음과 같은 요구사항을 만족해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 상호 배제 (Mutual Exclusion)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 한 번에 하나의 프로세스만이 임계 구역에 들어갈 수 있어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 진행 (Progress)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 임계 구역에 들어가려는 프로세스가 없을 때, 어느 프로세스도 임계 구역에 들어갈 수 있어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 한정 대기 (Bounded Waiting)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 각 프로세스는 임계 구역에 들어가기 위해 무한정 기다리지 않아야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;피터슨의 알고리즘(Peterson's Algorithm)은 위 요구사항들을 만족하는 고전적인 알고리즘으로, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;두 프로세스 간&lt;/b&gt;&lt;/span&gt;의 상호 배제를 위해 만들어졌습니다. 1980년대에 게리 피터슨(Gary L. Peterson)에 의해 제안됐으며, &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;상호 배제 문제를 간단하고 효율적으로, 이해하기 쉽고 최소한의 변수만을 사용하여 구현할 수 있다는 장점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;피터슨의 알고리즘은 flag 배열과 turn 변수를 사용하여 두 프로세스가 임계 영역에 동시에 진입(즉 경쟁 상태 발생)하는 것을 방지합니다. flag 배열은 각 프로세스가 임계 구역에 들어가기를 원하는지를 표시하며, turn 변수는 임계 구역에 들어갈 차례인 프로세스를 나타내는데요. 이 간단한 구조 덕에 직관적이고 구현하기 쉬운 상호 배제 해결책으로 널리 알려져 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;피터슨의 알고리즘 동작 순서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;피터슨의 알고리즘은 다음과 같은 순서로 각 작업이 순서대로 임계 영역에 들어가게 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 임계 구역 진입 요청&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 프로세스가 임계 구역(critical section)에 들어가고자 할 때, 자신의 flag를 true로 설정하여 진입 의사를 표시합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 양보 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 프로세스는 turn 변수를 상대방 프로세스의 번호로 설정하고, 이를 통해 상대방 프로세스에게 양보할 의사를 나타냅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 대기 상태&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 프로세스는 상대방 프로세스의 flag가 true이고, turn이 상대방 프로세스의 번호인 동안 계속해서 기다립니다(상대방 프로세스가 임계 구역에 들어가고자 할 경우 상대방에게 우선권을 주기 위함)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 임계 구역 진입&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 상대방 프로세스가 임계 구역에 들어가고자 하지 않거나, turn이 자신의 번호로 설정될 때까지 기다린 후 임계 구역에 진입합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. 임계 구역 종료&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 임계 구역에서의 작업을 마치면, 자신의 flag를 false로 설정하여 임계 구역에서 나왔음을 알립니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6. 비임계 구역 작업&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 비임계 구역에서 다른 작업을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드로 나타내면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719047306638&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변수 선언
boolean flag[2] = {false, false};
int turn;

function process0() {
    while (true) {
        // 1. 임계 구역 진입 요청
        flag[0] = true;
        
        // 2. 양보 설정
        turn = 1;
        
        // 3. 대기 상태
        while (flag[1] == true &amp;amp;&amp;amp; turn == 1) {
            // busy-wait
        }
        
        // 4. 임계 구역 진입
        // 임계 구역에서 수행할 작업
        print(&quot;Process 0 in critical section&quot;)

        // 5. 임계 구역 종료
        flag[0] = false;

        // 6. 비임계 구역 작업
        // 비임계 구역에서 수행할 작업
        print(&quot;Process 0 in non-critical section&quot;)
    }
}

function process1() {
    while (true) {
        // 1. 임계 구역 진입 요청
        flag[1] = true;
        
        // 2. 양보 설정
        turn = 0;
        
        // 3. 대기 상태
        while (flag[0] == true &amp;amp;&amp;amp; turn == 0) {
            // busy-wait
        }

        // 4. 임계 구역 진입
        // 임계 구역에서 수행할 작업
        print(&quot;Process 1 in critical section&quot;)

        // 5. 임계 구역 종료
        flag[1] = false;

        // 6. 비임계 구역 작업
        // 비임계 구역에서 수행할 작업
        print(&quot;Process 1 in non-critical section&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;피터슨의 알고리즘 문제점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 두 개 프로세스에서만 적용 가능&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 피터슨의 알고리짐은 기본적으로 두 개의 프로세스 사이에서만 상호 배제를 보장합니다. 2개보다 많은 프로세스를 다룰 경우 알고리즘을 확장해야 하지만, 코드가 복잡해지고 효율성이 떨어질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. Busy waiting&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;: 피터슨의 알고리즘은 다른 프로세스가 임계 영역에 들어가있는 동안 다른 프로세스는 무한 루프를 통해 대기(Busy waiting)하는데, 이는 CPU 자원의 낭비 및 효율성 저하의 원인이 됩니다. 또한 실시간 시스템인 경우, Busy waiting은 일종의 지연 현상을 초래할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS/운영체제</category>
      <category>공유 자원</category>
      <category>상호 배제</category>
      <category>운영체제</category>
      <category>임계 영역</category>
      <category>피터슨의 알고리즘</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/134</guid>
      <comments>https://jofestudio.tistory.com/134#entry134comment</comments>
      <pubDate>Sat, 22 Jun 2024 18:32:02 +0900</pubDate>
    </item>
    <item>
      <title>랭체인(LangChain)의 뜻과 구성 요소</title>
      <link>https://jofestudio.tistory.com/133</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;랭체인(LangChain)이란&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;대규모 언어 모델(LLM, Large Language Model)을 기반 앱 구축을 위한 오픈 소스 프레임워크를 말합니다. 원래 ChatGPT, LLama 등의 LLM을 활용한 애플리케이션을 구축하려면 다음과 같은 작업들을 해줘야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) 여러 LLM별로 인터페이스 별도 구성&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) LLM에게 했던 이전 대화 내용 기록을 별도로 저장 및 LLM에게 제공(컨텍스트를 맞춘다고도 합니다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3) 각종 Data source로부터 데이터를 수집해 LLM을 튜닝 또는 LLM에게 질의하는 파이프라인을 직접 구성&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4) 등등..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 랭체인은 이렇게 LLM 기반의 앱 개발을 좀 더 편하고 빠르게 해줄 수 있도록 하는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;프레임워크&quot;&lt;/b&gt;&lt;/span&gt;라고 보면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;랭체인 구성 요소&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) LLM 추상화&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우리가 객체지향 프로그래밍 시간에 배웠던 추상화 개념과 일치합니다. 자동차를 운전할 때, 어떤 차를 운전하든 핸들을 왼쪽으로 꺾으면 차가 왼쪽으로 가고 핸들을 오른쪽으로 꺾으면 차가 오른쪽으로 간다는 것을 우리는 알고 있습니다. 핸들의 움직임에 따라 내부적으로 어떻게 동작하는지는 우리가 알 필요가 없죠. 어떤 차든간에 핸들의 방향에 따라 차가 나아간다는 것만 알면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마찬가지로 현재 세상에 나온 여러 LLM들은 서로 특성 등이 상이하기 때문에, 애플리케이션에서 사용할 LLM별로 특성 등을 파악하는 것은 매우 번거롭고 힘든 일입니다. 따라서 랭체인은 LLM 추상화를 통해 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;어떤 LLM을 사용하던 간에 동일한 인터페이스를 우리에게 제공&lt;/b&gt;&lt;/span&gt;해줌으로써, 우리는 내부적으로 어떤 일이 일어나는지 신경쓸 필요 없이 앱을 구축할 수 있게 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 프롬프트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프롬프트는 LLM에 전달하는 명령어를 말합니다. 특히 랭체인에는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;프롬프트 템플릿 클래스&quot;&lt;/b&gt;&lt;/span&gt;라는 것이 있습니다. LLM에게 전달할 컨텍스트와 쿼리, 출력 형식 등을 직접 수동으로 작성할 필요 없이 자동으로 프롬프트의 구성을 구조화(템플릿화)해주는 것이라고 이해하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) 체인&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; LLM 기반 앱은 단순히 LLM에게 뭔가를 질의하고 끝나기보다는 여러 태스트들이 이어진 형태로 동작하는 경우도 많습니다. 예를 들어&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 특정 데이터 검색&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 검색된 데이터 요약&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 요약된 데이터 기반으로 LLM에게 질의&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;체인은 이 일련의 Task들이 서로 연결될 수 있도록 해주는 것이라고 이해하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4) 인덱스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;LLM들은 특정 날짜까지만(ex: 2023년 4월 등)의 데이터를 학습했기 때문에 상황에 따라 잘못된 답변을 내놓을 수 있습니다(이를 할루시네이션이라고도 부릅니다). 이 때 LLM에게 외부의 데이터를 직접 학습시켜서 보다 정확한 답을 내놓을 수 있도록 증강시킬 수 있는데 이를 RAG(Retrieval-Augmented Generation)이라 부릅니다. 이 RAG를 위해서는 외부 데이터 소스에 접근할 수 있는 기능이 필요하며, 랭체인에서는 이를 지원합니다. 이때 랭체인에서 이 외부 데이터들을 쉽게 탐색할 수 있도록 구조화하는 모듈을&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;인덱스&lt;/b&gt;&lt;/span&gt;라고 부릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5) 메모리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자가 프롬프트로 LLM과 질의를 주고받는 동안, 그 대화의 내용(컨텍스트)을 기억해 향후 적용할 수 있어야 합니다. 기본적으로 LLM은 채팅으로 직접 이전 대화에서의 컨텍스트를 전달해야 하지만, 랭체인은 애플리케이션에 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;메모리&quot;&lt;/b&gt;&lt;/span&gt;를 추가해 직접 채팅으로 컨텍스트를 전달하지 않고도 LLM이 컨텍스트를 기억하도록 해준다고 이해하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;6) 에이전트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;LLM과 하나 이상의 도구를 결합해, LLM을 통해 수행할 일련의 작업을 선택하는 것이라고 이해하면 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고글&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@ywkim36/147&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://brunch.co.kr/@ywkim36/147&lt;/a&gt;&lt;/p&gt;</description>
      <category>AI/LangChain</category>
      <category>langchain</category>
      <category>랭체인</category>
      <category>메모리</category>
      <category>에이전트</category>
      <category>인덱스</category>
      <category>체인</category>
      <category>프롬프트</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/133</guid>
      <comments>https://jofestudio.tistory.com/133#entry133comment</comments>
      <pubDate>Sun, 16 Jun 2024 17:14:42 +0900</pubDate>
    </item>
    <item>
      <title>[회고] 출근길에 금융 포스트들을 보내주는 서비스 제작기</title>
      <link>https://jofestudio.tistory.com/132</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;들어가며&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;작년 말 운좋게 금융권 인턴 취업에 성공하고, 올해 무사히 정직원 전환에 성공했습니다. 금융권에 재직 중이고, 돈을 벌기 시작하면서 금융과 경제에 대한 관심이 자연스럽게 많아지고 있는데요. 문득 출근길 지하철에서 금융 포스트들을 하나씩이라도 읽으면 좋겠다는 생각이 들었고, 매일 아침 출근길마다 읽기 좋은 금융 포스트들의 링크를 보내주는 서비스들을 만들면 어떨까라는 생각으로 이어졌습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그와 동시에 실제로 이 서비스를 간단히 운영해보고 싶다는 생각이 들었고, 운영 과정에서 발생하는 비용을 최소화하는 걸 목표로 삼았는데요. 이 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;출근길 금융 한 잔&lt;/b&gt;&lt;/span&gt;&quot;이라는 간단한 서비스를 개발한 과정을 소개해드리려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 어떤 플랫폼의 포스트들을 보낼까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;첫 번째로는 어떤 글들을 보낼 것인가를 고민했습니다. 아무래도 스스로를 위한 서비스인만큼 제가 잘 읽힌다고 생각되는 플랫폼들을 선정했는데요. 고민 끝에, 카카오뱅크와 토스뱅크에서 운영하는 플랫폼의 글들을 보내주기로 결정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@kakaobank#articles&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://brunch.co.kr/@kakaobank#articles&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716020407402&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;이미 모두의 은행. 카카오뱅크 공식 브런치입니다.&quot; data-og-host=&quot;brunch.co.kr&quot; data-og-source-url=&quot;https://brunch.co.kr/@kakaobank#articles&quot; data-og-url=&quot;https://brunch.co.kr/@kakaobank&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MoXWT/hyV6glplNL/fXcOnnPWEfHxWY7eVBBkmk/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/btrRgG/hyV6b5rnTr/ltspYXYBsIPiMslmiZFuhk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@kakaobank#articles&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://brunch.co.kr/@kakaobank#articles&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MoXWT/hyV6glplNL/fXcOnnPWEfHxWY7eVBBkmk/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/btrRgG/hyV6b5rnTr/ltspYXYBsIPiMslmiZFuhk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;카카오뱅크의 브런치스토리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이미 모두의 은행. 카카오뱅크 공식 브런치입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;brunch.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://blog.toss.im/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.toss.im/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716020424810&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;콘텐츠도 토스가 만들면 다릅니다&quot; data-og-host=&quot;blog.toss.im&quot; data-og-source-url=&quot;https://blog.toss.im/&quot; data-og-url=&quot;https://blog.toss.im/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXTTvo/hyV59fuXDY/JKJ2rXz82KN9vWKXPk3L5K/img.jpg?width=2400&amp;amp;height=1620&amp;amp;face=0_0_2400_1620,https://scrap.kakaocdn.net/dn/diZAD2/hyV6jJfe4u/Kw7YXxd7lmyKSB5t9YwIA0/img.jpg?width=2400&amp;amp;height=1620&amp;amp;face=0_0_2400_1620,https://scrap.kakaocdn.net/dn/ceZ4lf/hyV6eA7urZ/OvO64ywfOIdmZbvwJ0ggA0/img.jpg?width=3201&amp;amp;height=2161&amp;amp;face=0_0_3201_2161&quot;&gt;&lt;a href=&quot;https://blog.toss.im/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.toss.im/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXTTvo/hyV59fuXDY/JKJ2rXz82KN9vWKXPk3L5K/img.jpg?width=2400&amp;amp;height=1620&amp;amp;face=0_0_2400_1620,https://scrap.kakaocdn.net/dn/diZAD2/hyV6jJfe4u/Kw7YXxd7lmyKSB5t9YwIA0/img.jpg?width=2400&amp;amp;height=1620&amp;amp;face=0_0_2400_1620,https://scrap.kakaocdn.net/dn/ceZ4lf/hyV6eA7urZ/OvO64ywfOIdmZbvwJ0ggA0/img.jpg?width=3201&amp;amp;height=2161&amp;amp;face=0_0_3201_2161');&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;콘텐츠도 토스가 만들면 다릅니다&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.toss.im&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 이 플랫폼의 포스트들을 어떻게 가져올까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음에는 크롤링을 통해 포스트들을 감싸고 있는 &amp;lt;ul&amp;gt;태그나 &amp;lt;ol태그&amp;gt;를 찾은 뒤, 그 안에 있는 목록들을 가져오는 방법을 구상했습니다. 그러다가 문득 &lt;b&gt;&quot;카카오뱅크나 토스뱅크도 처음에 블로그에 들어갈 때 특정 API를 통해서 포스트 목록들을 가져오지 않을까?&quot;&lt;/b&gt;라는 생각이 들더라구요. 개발자 도구 등을 사용해서 그 API를 제가 딸수만 있다면 크롤링보다 훨씬 적은 공수로 포스트들을 가져와서 활용할 수 있겠다는 생각에 바로 개발자 도구를 켰습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;3378&quot; data-origin-height=&quot;1826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIpnoG/btsHvts1Wec/utV1zouAWBl8GSKnHP2Kq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIpnoG/btsHvts1Wec/utV1zouAWBl8GSKnHP2Kq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIpnoG/btsHvts1Wec/utV1zouAWBl8GSKnHP2Kq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIpnoG%2FbtsHvts1Wec%2FutV1zouAWBl8GSKnHP2Kq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3378&quot; height=&quot;1826&quot; data-origin-width=&quot;3378&quot; data-origin-height=&quot;1826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Network 탭을 까본 결과, 카카오뱅크와 토스뱅크가 사용하는 API들을 어렵지 않게(?) 따올 수 있었습니다. 그러면 그&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; API의 응답값을 토대로 특정 포스트의 no값이나 key값을 딸수가 있었는데요, 카카오뱅크와 토스뱅크의 블로그들은 그 값들을 이용해서 다음과 같은 URL로 접근하면 해당 글들을 볼 수 있는 것까지 살펴볼 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;카카오뱅크 : https://brunch.co.kr/@kakaobank/{no}&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;토스뱅크 : https://blog.toss.im/article/{key}&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 포스트들의 링크를 어떤 매체로 전달할까?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;포스트들의 링크를 얻는 방법을 알았으니, 이 링크를 어떤 매체로 보낼지가 고민됐습니다. 가장 대중적인 매체인 &quot;카카오톡&quot;으로 보내보자는 생각이 들었고, 바로 채널을 개설했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2036&quot; data-origin-height=&quot;1702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OoFLH/btsHwdjTxrf/kOekA007AY0CnwbSTCK2i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OoFLH/btsHwdjTxrf/kOekA007AY0CnwbSTCK2i0/img.png&quot; data-alt=&quot;나름 채널 프로필도 포토샵으로 만들어봤습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OoFLH/btsHwdjTxrf/kOekA007AY0CnwbSTCK2i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOoFLH%2FbtsHwdjTxrf%2FkOekA007AY0CnwbSTCK2i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;509&quot; data-origin-width=&quot;2036&quot; data-origin-height=&quot;1702&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나름 채널 프로필도 포토샵으로 만들어봤습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;1620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6Zyc/btsHxsf5ut0/TAkRef5JqBQkLVTajWh4D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6Zyc/btsHxsf5ut0/TAkRef5JqBQkLVTajWh4D0/img.png&quot; data-alt=&quot;채널 개설 후 테스트 메시지 발송도 해봤답니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6Zyc/btsHxsf5ut0/TAkRef5JqBQkLVTajWh4D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6Zyc%2FbtsHxsf5ut0%2FTAkRef5JqBQkLVTajWh4D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;272&quot; height=&quot;584&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;1620&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채널 개설 후 테스트 메시지 발송도 해봤답니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만, 알람톡을 보내려면 일반 채널이 아닌 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;비즈니스 채널로 만들어야 한다&lt;/b&gt;&lt;/span&gt;는 것을 알게 됐습니다. 비즈니스 채널 개설을 위해선 사업자 등록증이 필요한데, 이걸 발급받고 이것저것 하는 것은 공수가 많이 들 것 같다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 카카오톡 채널을 통한 발송은 포기하고, 다른 방법을 알아봤습니다. 눈에 들어온 방법은 SMS 전송이었는데요, 다음과 같이 SMS 전송을 지원하는 4개의 플랫폼들을 비교했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;가비아&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoolSMS&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.8837%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;AWS SNS&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;네이버 클라우드 Notification&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;비용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; text-align: center;&quot;&gt;최소 충전 20,500원&lt;br /&gt;(건당 20.5원)&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;최소 충전 10,000원&lt;br /&gt;(건당 20원)&lt;/td&gt;
&lt;td style=&quot;width: 19.8837%; text-align: center;&quot;&gt;건당 약 30원&lt;br /&gt;(24.05.18 기준)&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%; text-align: center;&quot; rowspan=&quot;3&quot;&gt;사업자만 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;API 지원&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;width: 19.8837%; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기타&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.3721%; text-align: center;&quot;&gt;발신번호 등록 필요&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;발신번호 등록 필요&lt;/td&gt;
&lt;td style=&quot;width: 19.8837%; text-align: center;&quot;&gt;발신번호 등록 필요 X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SMS 발송에 필요한 발신번호는 &quot;아톡&quot; 등의 플랫폼에서 발급받을 수 있지만 월 2,000원이라는 비용이 발생합니다. 하지만 저는 이 서비스를 어차피 저(아니면 주변 사람들 몇 명만 더해서)만 사용할 것이기 때문에 발신번호는 제 010번호를 쓰면 되므로, 발신번호 필요 여부는 그리 큰 문제가 되지 않겠다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러면 비용만 비교하면 됐었는데요. 최소 충전 비용이 있다보니 1년도 채 안 지나서 서비스 운영을 종료할 예정이라면 AWS SNS를 쓰는게 이득이지만, 앵간하면 이 서비스는 제가 계속 쓸 것 같다는 생각이 들었습니다. 금융 관련 글은 읽으면 읽을수록 플러스지 마이너스는 안 될 거라고 생각하거든요. 결국, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;건당 비용이 가장 낮은 CoolSMS를 사용하기로 결정&lt;/b&gt;&lt;/span&gt;했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇다면, 실제로 CoolSMS를 통해 SMS를 보낼 수 있다는 걸 검증해야겠죠. 제공되는 API 문서를 통해 간단히 테스트했고, 제가 원하는 내용을 보낼 수 있음을 다음과 같이 검증할 수 있었습니다.&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;1179&quot; data-origin-height=&quot;1027&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7T6iy/btsHGUbC7no/ix5YaeK0su4628LXZyk6j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7T6iy/btsHGUbC7no/ix5YaeK0su4628LXZyk6j1/img.png&quot; data-alt=&quot;성공적인 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7T6iy/btsHGUbC7no/ix5YaeK0su4628LXZyk6j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7T6iy%2FbtsHGUbC7no%2Fix5YaeK0su4628LXZyk6j1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;411&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;1027&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공적인 테스트 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※ &lt;/span&gt;&lt;/span&gt;참고 : 빠른 속도를 위해 Python을 사용해서 개발 및 테스트했습니다&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4.&amp;nbsp; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;상세 로직은 어떻게 구성할까?&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;API를 통해 SMS를 보낼 수 있다는 것도 검증했으니, 이제 실질적으로 어떤 과정을 통해 포스트들의 링크를 SMS로 전달할지를 구상할 차례인데요. 우선 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음 3가지의 데이터는 가지고 있어야 한다고 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) SMS를 받을 사람(구독자들)의 휴대폰 번호&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) 메시지 내용 템플릿&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3) 가장 최근에 보낸 포스트의 정보(카카오뱅크는 no, 토스뱅크는 key)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 바탕으로, &lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;초기에는 다음과 같은 로직을 구상했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;1388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE3VZM/btsHFvYlVJE/TwKMHCM8TDgK5qF7IgCkdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE3VZM/btsHFvYlVJE/TwKMHCM8TDgK5qF7IgCkdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE3VZM/btsHFvYlVJE/TwKMHCM8TDgK5qF7IgCkdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE3VZM%2FbtsHFvYlVJE%2FTwKMHCM8TDgK5qF7IgCkdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;1388&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;1388&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2번 과정에서 마지막(가장 최근)에 보낸 포스트들의 정보를 가져오고, 이를 토대로 어떤 포스트들을 보내야할지를 4번 과정에서 추린 다음 SMS를 전송하는 로직입니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 이렇게 되면, 네트워크 문제 등으로 6번 과정(SMS 전송)에 실패해도 이미 실행된 5번 과정에 의해 전송된 포스트 정보가 갱신된다는 문제가 있었습니다. 즉 특정 포스트는 건너 뛰게 되는 현상이 발생할 수 있었죠. 따라서 5번과 6번의 순서를 바꿔서, 다음과 같은 로직으로 구성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2678&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RSAW2/btsHOGd8bQI/KrrETcWaOAnBvsc0VKTW0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RSAW2/btsHOGd8bQI/KrrETcWaOAnBvsc0VKTW0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RSAW2/btsHOGd8bQI/KrrETcWaOAnBvsc0VKTW0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRSAW2%2FbtsHOGd8bQI%2FKrrETcWaOAnBvsc0VKTW0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2678&quot; height=&quot;1400&quot; data-origin-width=&quot;2678&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. &lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;어떤 DB를 사용할까?&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 데이터들을 담을 DB를 골라야 했습니다. 클라우드 서비스들을 알아봤는데요. 제 AWS 계정은 프리 티어를 사용할 수 있는 시기가 이미 지났기 때문에 AWS가 아닌 다른 서비스들을 찾아봤고, 서치를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Supabase&lt;/b&gt;&lt;/span&gt;라는 BaaS(Backend as a Service)를 알게 됐습니다. 완전 무료형 서비스는 아니고 500MB만큼의 DB 용량을 무료로 사용 가능한데, 어차피 저는 DB에 insert가 아닌 update만 해줄 거라 용량제한을 넘길 일은 없고, 끽해야 하루에 한 번 read/write를 하기 때문에 이 녀석을 쓰는게 딱이겠다는 생각이 들었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;역시나 테스트를 해봐야겠죠? Supabase 콘솔에서 테이블들을 세팅해주고, 다음 코드들을 통해 select(휴대폰 번호 테이블에서 번호들 가져오기)와 update(가장 최근에 보낸 포스트 정보 갱신)가 정상 수행되는 걸 확인했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717676921575&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# SMS를 받을 사람들의 핸드폰 번호 가져오기
subscriber_phone_numbers = supabase.table(os.environ.get(&quot;PHONE_NUMBER&quot;))\
    			.select(&quot;*&quot;).execute().data

print(subscriber_phone_numbers)

# [
#   {&quot;phone_number&quot;: &quot;010XXXXXXXX},
#   {&quot;phone_number&quot;: &quot;010XXXXXXXX},
#   {&quot;phone_number&quot;: &quot;010XXXXXXXX}
# ]


# 가장 최근에 보낸 포스트의 정보 갱신(토스뱅크)
supabase.table(os.environ.get(&quot;TOSS_BANK_POST&quot;))\
                .update({&quot;toss_last_send_key&quot;: key}).eq(&quot;id&quot;, id).execute()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;6. 서버는 어디에 띄울까?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드 작성은 끝났고, 이제 가장 중요한 단계죠. 서버를 어디에 띄울지 고민했습니다. 아무래도 매일 아침에만 코드가 실행되면 되게 때문에 서버리스를 도입하는게 궁극적인 목적인 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;비용 절감&quot;&lt;/b&gt;&lt;/span&gt;에 가장 적합하다고 생각했는데요. 그렇다고 처음부터 서버리스만 고려하기 보다는 EC2에 호스팅하는 방법을 포함해서 다음과 같이 4가지 방법을 조사했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.2325%; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 24.4187%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;AWS EC2 + Scheduler&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.6511%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;AWS Lambda + EventBridge&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.6279%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Supabase Edge Fuction + pg_cron&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.0698%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;스크립트 페이지 호스팅 + cron_job.org&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.2325%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비용&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.4187%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1,586원/월&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(스팟 인스턴스, t3.nano 기준)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.6511%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3.55원/월&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(0.5GB 메모리, 10초 실행 기준)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.6279%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;무료&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.0698%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;무료&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.2325%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비고&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.4187%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) EC2를 Public Subnet에 두면 보안 취약&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) EC2를 Private Subnet에 두면 NAT Gateway 비용 추가 발생&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.6511%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 21.6279%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;TypeScript로만 작성 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.0698%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다른 사람이 해당 페이지를 호출함으로써 스크립트 실행 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) AWS EC2 + Scheduler&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;EC2 상에 서버를 띄우고, 자체적으로 스케쥴러(스프링의 @Scheduled 등)를 설정해 일마다 수행하는 방법입니다. EC2는 사용한 만큼 지불하는 On-demand 옵션 외에도 할인된 금액으로 선납입을 하는 예약 인스턴스, 가장 저렴한 금액으로 사용 가능하나 서비스가 중단될 가능성이 있는 스팟 인스턴스 등의 유형이 있는데요. 본 서비스는 서비스가 중단되도 큰 영향이 없기 때문에(다음날 수행되면 어차피 전날 안 보내진 것도 일괄 전송되므로) 스팟 인스턴스를 사용해도 무방하다고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1710&quot; data-origin-height=&quot;1042&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beiUGO/btsHNYfhZmC/kSEKRmiXGjekZ34nkow270/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beiUGO/btsHNYfhZmC/kSEKRmiXGjekZ34nkow270/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beiUGO/btsHNYfhZmC/kSEKRmiXGjekZ34nkow270/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeiUGO%2FbtsHNYfhZmC%2FkSEKRmiXGjekZ34nkow270%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;520&quot; height=&quot;317&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;1042&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;24년 6월 4일 기준으로, t3.nano를 사용할 때 시간당 0.0016 달러를 내는데요. 24를 곱하고 30을 곱하면, &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한달에 1.152달러 = 1,586원이 발생됩니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※ 참고 : &lt;/span&gt;On-demand의 경우, 이 케이스에선 4배 정도 더 비쌈&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 EC2는 최초 생성 시 Public Subnet에 둘 건지 Private Subnet에 둘 건지를 정해야 합니다. 해당 EC2는 외부로부터의 선접근이 없으므로 Private Subnet에 두는 게 보안상 안전하나, 카뱅 &amp;amp; 토뱅으로부터 최신 포스트 목록을 가져오는 과정, supabase에 접근해 데이터를 가져오는 과정 등에서 외부와의 통신이 필요하므로 NAT 게이트웨이 등을 별도 설치해야 하고, 이는 추가 비용을 야기합니다. 그렇다고 Public Subnet에 두자니, 외부로부터의 접근에 노출될 우려가 있습니다. 따라서 EC2와 Scheduler 조합을 선택할 경우, 이 부분을 좀 더 고려해서 선택해야 할 필요가 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) AWS Lambda + EventBridge&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AWS의 서버리스 서비스인 Lambda Function을 등록하고, EventBridge를 통해 일마다 Lambda를 호출하는 방법입니다. 제 로컬을 기준으로 1회 실행 시 3.2초 정도가 수행됐는데요, 아무래도 보수적으로 1회 실행에 10초가 걸린다고 잡고 512MB짜리 메모리를 쓴다고 가정했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1638&quot; data-origin-height=&quot;732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A8DHR/btsHNYl5DXv/8Oil3jnDzl8QC3pt9ffVuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A8DHR/btsHNYl5DXv/8Oil3jnDzl8QC3pt9ffVuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A8DHR/btsHNYl5DXv/8Oil3jnDzl8QC3pt9ffVuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA8DHR%2FbtsHNYl5DXv%2F8Oil3jnDzl8QC3pt9ffVuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;269&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;732&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일마다 실행하므로 한 달에 30번만 실행되기 때문에, 30 x 10(10초) x 0.5(512MB = 0.5GB) x 0.0000166667 = 0.0025달러 = 3.5원 정도의 비용이 한 달마다 발생되게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 EventBridge에 대한 비용도 고려해야 하는데요.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1822&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R3OY2/btsHOX06cmw/SC0rVt7lrfv7XkuMO4Oma1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R3OY2/btsHOX06cmw/SC0rVt7lrfv7XkuMO4Oma1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R3OY2/btsHOX06cmw/SC0rVt7lrfv7XkuMO4Oma1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR3OY2%2FbtsHOX06cmw%2FSC0rVt7lrfv7XkuMO4Oma1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;642&quot; height=&quot;203&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;100만건의 스케줄러 호출 당 1.15달러의 비용이 월마다 발생하는데요. 우리는 월 30번만 호출하니 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;0.0000345달러 = 약 0.05원의 비용이 달마다 발생합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;즉, 정리하면 AWS Lambda + EventBridge 사용 시, 3.55원의 비용이 월마다 발생합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) Supabase Edge Function + pg_cron&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Lambda와 같은 서버리스 서비스인 Supabase Edge Function을 등록하고, pg_cron이라는 Supabase Extension을 통해 일마다 Edge Function을 호출하는 방법입니다.  24년 6월 4일을 기준으로 Edge Function은 TypeScript만으로만 작성가능하고, HTTP 요청을 통해 Edge Function을 호출하는 방식으로 동작합니다. 따라서 Edge Function과 + pg_cron을 선택하는 경우 HTTP 요청에 대한 별도의 인증을 구성하지 않는다면 제 3자의 Edge Function 호출로  SMS 발송이 야기되어 과금이 발생할 수 있으니 꼭 인증을 구성할 필요가 있고, TypeScript로 다시 코드를 작성해야 하는 공수가 발생함을 고려해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4) 스크립트 페이지 호스팅 + cron_job.org&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우비나 닷홈 등의 무료 호스팅 서비스를 이용해 스크립트 페이지를 만들고, cron_job.org라는 무료 스케쥴러 사이트를 활용해 일마다 스크립트 페이지로 HTTP Request를 보내게 하는 방법입니다. 완전 무료로 사용 가능하지만, 호스팅된 스크립트 페이지를 다른 사람도 호출할 수가 있는데요. 이 경우 제가 원치 않는 타이밍에 SMS가 발송되고, 이것들이 모두 과금으로 이어질 수 있다는 치명적인 단점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리하면, EC2 사용은 월 1,500원 이상의 비용이 발생하니 고려 대상에서 제외했고, 스크립트 페이지 호스팅 &amp;amp; cron_job.org 사용은 제 3자의 스크립트 페이지를 요청으로 인한 과금 폭탄이 발생 가능해 제외했습니다. AWS Lambda와 Supabase Edge Function이 남았는데, Lambda 사용 시 발생하는 월 3원 가량의 비용은 사실상 없는 수준이라고 생각했고 Supabase Edge Function은 제가 사용해본 적 없는 TypeScript로 코드를 재구성해야 하는 점 등의 공수가 걸렸습니다. 따라서 최종적으로 Lambda와 EventBridge를 사용하는 것으로 결정하게 됐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;7. AWS Lambda 함수 생성 &amp;amp; EventBridge 설정 (with. 트러블 슈팅..)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Lambda 함수 생성시, 파이썬 버전은 로컬에서와 동일하게 3.11버전으로 설정해줬습니다.&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;1718&quot; data-origin-height=&quot;1454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m8KVb/btsHQka7ESS/QBGPlcW1ZKT0CigbioBbbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m8KVb/btsHQka7ESS/QBGPlcW1ZKT0CigbioBbbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m8KVb/btsHQka7ESS/QBGPlcW1ZKT0CigbioBbbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm8KVb%2FbtsHQka7ESS%2FQBGPlcW1ZKT0CigbioBbbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;541&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;1454&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 뒤 함수에서 사용될 환경변수들을 세팅해줬습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-06 오후 4.23.22.png&quot; data-origin-width=&quot;2532&quot; data-origin-height=&quot;1364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qFWVv/btsHPnT9d6O/PtKNhtuOzAe9Xt3nyBKJT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qFWVv/btsHPnT9d6O/PtKNhtuOzAe9Xt3nyBKJT0/img.png&quot; data-alt=&quot;값은 가렸습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qFWVv/btsHPnT9d6O/PtKNhtuOzAe9Xt3nyBKJT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqFWVv%2FbtsHPnT9d6O%2FPtKNhtuOzAe9Xt3nyBKJT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;741&quot; height=&quot;399&quot; data-filename=&quot;스크린샷 2024-06-06 오후 4.23.22.png&quot; data-origin-width=&quot;2532&quot; data-origin-height=&quot;1364&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;값은 가렸습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 Lambda 함수가 사용할 외부 라이브러리들을 세팅해줘야 했는데요. &quot;레이어&quot;를 통해 이것이 가능하고, 쉽게 설명하면 제가 세팅해준 레이어 위에서 Lambda함수가 돌아가는 개념입니다.(주로 이렇게 외부 라이브러리들을 미리 세팅할 용도로 활용한다고 합니다) 로컬에서 쓴 라이브러리들을 pip freeze &amp;gt;를 통해 requirements.txt로 빼준 뒤, pip install -t를 통해서 해당 라이브러리들을 따로 제가 지정한 폴더에 설치한 후 해당 폴더를 압축해서 레이어 생성에 활용할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;3338&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K9vFS/btsHRvbF6lC/qm9eJGtEr1zDLHNMCyluzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K9vFS/btsHRvbF6lC/qm9eJGtEr1zDLHNMCyluzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K9vFS/btsHRvbF6lC/qm9eJGtEr1zDLHNMCyluzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK9vFS%2FbtsHRvbF6lC%2Fqm9eJGtEr1zDLHNMCyluzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3338&quot; height=&quot;892&quot; data-origin-width=&quot;3338&quot; data-origin-height=&quot;892&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;1862&quot; data-origin-height=&quot;1850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cND6kP/btsHO4HkcIg/M0Mp7C5bGyhpKfs8PLJZg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cND6kP/btsHO4HkcIg/M0Mp7C5bGyhpKfs8PLJZg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cND6kP/btsHO4HkcIg/M0Mp7C5bGyhpKfs8PLJZg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcND6kP%2FbtsHO4HkcIg%2FM0Mp7C5bGyhpKfs8PLJZg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;671&quot; height=&quot;667&quot; data-origin-width=&quot;1862&quot; data-origin-height=&quot;1850&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 다음, 이렇게 생성한 레이어를 아까 만들어둔 Lambda 함수에 추가해줬습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mUNlm/btsHQfA5U1S/fIo3Rkn6MY9EZ6j4JkkkCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mUNlm/btsHQfA5U1S/fIo3Rkn6MY9EZ6j4JkkkCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mUNlm/btsHQfA5U1S/fIo3Rkn6MY9EZ6j4JkkkCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmUNlm%2FbtsHQfA5U1S%2FfIo3Rkn6MY9EZ6j4JkkkCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;548&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;1456&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 만들어둔 코드를 Lambda 코드에 복붙한 뒤 테스트 실행을 눌렀는데, 다음과 같이 pydantic_core라는 모듈을 찾을 수 없다는 오류를 만났습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717660911554&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Response
{
  &quot;errorMessage&quot;: &quot;Unable to import module 'lambda_function': No module named 'pydantic_core._pydantic_core'&quot;,
  &quot;errorType&quot;: &quot;Runtime.ImportModuleError&quot;,
  &quot;requestId&quot;: &quot;Blah Blah ~&quot;,
  &quot;stackTrace&quot;: []
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※ &lt;/span&gt;&lt;/span&gt;관련된 스택 오버플로 글&amp;nbsp;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/76650856/no-module-named-pydantic-core-pydantic-core-in-aws-lambda-though-library-is-i&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/76650856/no-module-named-pydantic-core-pydantic-core-in-aws-lambda-though-library-is-i&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1717673424200&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;No module named 'pydantic_core._pydantic_core' in AWS Lambda though library is installed for fast api based code&quot; data-og-description=&quot;AWS lambda deployment of FastAPI gives the following error: [ERROR] Runtime.ImportModuleError: Unable to import module 'users_crud': No module named 'pydantic_core._pydantic_core' Traceback (most r...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/76650856/no-module-named-pydantic-core-pydantic-core-in-aws-lambda-though-library-is-i&quot; data-og-url=&quot;https://stackoverflow.com/questions/76650856/no-module-named-pydantic-core-pydantic-core-in-aws-lambda-though-library-is-i&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/h1ZN4/hyWhaLMNvY/blT0EDSlncX9C0wPci7KeK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/uFZnD/hyWg03qZUh/sAbv3jEjaJrEpjVr95jJ8K/img.png?width=849&amp;amp;height=561&amp;amp;face=0_0_849_561&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/76650856/no-module-named-pydantic-core-pydantic-core-in-aws-lambda-though-library-is-i&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/76650856/no-module-named-pydantic-core-pydantic-core-in-aws-lambda-though-library-is-i&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/h1ZN4/hyWhaLMNvY/blT0EDSlncX9C0wPci7KeK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/uFZnD/hyWg03qZUh/sAbv3jEjaJrEpjVr95jJ8K/img.png?width=849&amp;amp;height=561&amp;amp;face=0_0_849_561');&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;No module named 'pydantic_core._pydantic_core' in AWS Lambda though library is installed for fast api based code&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AWS lambda deployment of FastAPI gives the following error: [ERROR] Runtime.ImportModuleError: Unable to import module 'users_crud': No module named 'pydantic_core._pydantic_core' Traceback (most r...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요약하면 Lambda는 특정 아키텍쳐에 맞는 패키지들을 필요로 하는데, 제가 레이어로 세팅해준 라이브러리들이 Lambda가 구동되는 아키텍쳐와 맞지 않아서 발생되는 문제였습니다. 실제로 제 로컬에서 requirements.txt에 적힌 라이브러리들에 대해 pip install을 해보면,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-06 오후 6.07.57.png&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n87C9/btsHRaTcsWZ/F4dO9jJ8hElvkHhAJRxgT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n87C9/btsHRaTcsWZ/F4dO9jJ8hElvkHhAJRxgT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n87C9/btsHRaTcsWZ/F4dO9jJ8hElvkHhAJRxgT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn87C9%2FbtsHRaTcsWZ%2FF4dO9jJ8hElvkHhAJRxgT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2114&quot; height=&quot;602&quot; data-filename=&quot;스크린샷 2024-06-06 오후 6.07.57.png&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 특정 라이브러리들이 제 로컬 머신의 아키텍쳐(macos)에 맞는 버전으로 설치되는 걸 볼 수 있었습니다.(pip install을 할 때 라이브러리들이 머신의 아키텍쳐 버전에 맞춰 설치되는 걸 처음 알았네요... 모든 라이브러리가 이렇게 아키텍쳐별로 버전을 다르게 제공하는 건 아닙니다) 하지만 저는 Lambda함수를 생성할 때 아키텍쳐를 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;x86_64로&lt;/span&gt; 설정했으니 오류가 발생하는 거였죠. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서 다음 명령어를 통해 라이브러리들을 x86_64 아키텍쳐에 맞는 버전으로 설치하게끔 하고, 이걸 다시 압축해서 새로 레이어를 세팅해줬습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717673943785&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install -r requirements.txt --platform manylinux2014_x86_64 --target ./python --only-binary=:all:&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 결과,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1230&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/43yQH/btsHQ9mqORn/tQmvyW1y5eixX9AwsUDWi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/43yQH/btsHQ9mqORn/tQmvyW1y5eixX9AwsUDWi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/43yQH/btsHQ9mqORn/tQmvyW1y5eixX9AwsUDWi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F43yQH%2FbtsHQ9mqORn%2FtQmvyW1y5eixX9AwsUDWi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;718&quot; height=&quot;339&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;580&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;1179&quot; data-origin-height=&quot;2556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/onXi3/btsHP2olrBp/lfKGoxNtDkkPTfcVow3h30/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/onXi3/btsHP2olrBp/lfKGoxNtDkkPTfcVow3h30/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/onXi3/btsHP2olrBp/lfKGoxNtDkkPTfcVow3h30/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FonXi3%2FbtsHP2olrBp%2FlfKGoxNtDkkPTfcVow3h30%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;286&quot; height=&quot;620&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Lambda를 통해 제가 짰던 코드가 정상적으로 실행되게 할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 EventBridge를 통해 생성한 Lambda함수를 스케줄링할 차례입니다. 이건 매우 간단했는데요. 생성된 Lambda 함수 콘솔에서 트리거 추가를 통해 설정해줄 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2160&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw617z/btsHQjwD4aU/35isFYrK3vIkIFj50vXMTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw617z/btsHQjwD4aU/35isFYrK3vIkIFj50vXMTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw617z/btsHQjwD4aU/35isFYrK3vIkIFj50vXMTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw617z%2FbtsHQjwD4aU%2F35isFYrK3vIkIFj50vXMTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;752&quot; height=&quot;362&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1674&quot; data-origin-height=&quot;1712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dq3GMz/btsHQ8nx5mi/PiDCZeNgezfd8C9byC9wTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dq3GMz/btsHQ8nx5mi/PiDCZeNgezfd8C9byC9wTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dq3GMz/btsHQ8nx5mi/PiDCZeNgezfd8C9byC9wTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdq3GMz%2FbtsHQ8nx5mi%2FPiDCZeNgezfd8C9byC9wTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;632&quot; height=&quot;646&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;1712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 cron 표현식은 월-금요일 매일 12시에 실행한다는 의미입니다. 다만 UTC가 기준이기 때문에, 한국 시각으로는 21시에 실행됩니다(KST가 UTC보다 9시간 앞서 있기 때문). 따라서 저렇게 트리거를 추가한뒤, 실제로 21시에 람다가 실행되는지를 봤습니다. 그 결과,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1179&quot; data-origin-height=&quot;2556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIcd4X/btsHQIv4dij/bzVD9YKKeLHVcfHnJBh8n0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIcd4X/btsHQIv4dij/bzVD9YKKeLHVcfHnJBh8n0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIcd4X/btsHQIv4dij/bzVD9YKKeLHVcfHnJBh8n0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIcd4X%2FbtsHQIv4dij%2FbzVD9YKKeLHVcfHnJBh8n0%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;300&quot; height=&quot;650&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 21시에 Lambda가 수행되어, SMS가 정상 전송된 것을 확인할 수 있었습니다!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 매일 아침 08시에 람다가 실행되도록 EventBridge 규칙의 cron 표현식을 다음과 같이 변경해주고 마무리 지었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717675777676&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cron(0 23 ? * SUN-THU *)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;마무리...&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;되게 간단한 서비스라고 생각했는데, 이것저것 고민하는 과정들이 많았습니다.. 만들었다 하기도 뭐하고 운영이라 하기도 뭐하지만, 그래도 생각해보니 제가 만든 서비스를 이렇게 운영 레벨로 옮겨온 건 처음이더라구요. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;옛날에 이두희님이 코딩으로 자기가 만들고 싶은 것들을 뚝딱뚝딱 만드는 걸 보며 되게 멋있다고 생각했는데, 저도 제가 나한테 필요하겠다 싶은 서비스를 이렇게&amp;nbsp;&lt;/span&gt;중간에 멈추지 않고 처음 목표대로 실제 운영 단계까지 오는데 성공해서 굉장히 뿌듯...&amp;gt;&amp;lt; 합니다. 우선 저랑 여자친구한테만 이 글들이 가게 하고, 나중에 친구들 몇 명 해서 추가할 것 같네요. 일련의 과정들을 정리하고 공유하고 싶어 글을 이렇게 써봤는데, 난잡한 글 끝까지 봐주셔서 감사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/개발일지</category>
      <category>AWS</category>
      <category>CoolSMS</category>
      <category>EventBridge</category>
      <category>lambda</category>
      <category>pydantic_core</category>
      <category>개발</category>
      <category>금융</category>
      <category>람다</category>
      <category>운영</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/132</guid>
      <comments>https://jofestudio.tistory.com/132#entry132comment</comments>
      <pubDate>Thu, 6 Jun 2024 21:32:38 +0900</pubDate>
    </item>
    <item>
      <title>리눅스 awk 커맨드 사용법 및 동작 원리</title>
      <link>https://jofestudio.tistory.com/131</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;awk 커맨드란&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;awk programming language라는 프로그래밍 언어로 작성된 프로그램을 실행하는 커맨드.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 파일들로부터 레코드(행)들을 읽어들여서 어떤 값을 조작하거나 데이터화하는 것을 목적으로 패턴 매칭 검사 등에 활용한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SQL 쿼리를 통해 내가 원하는 테이블의 row들을 하나씩 보면서 where 조건절에 부합하는 애들만 뽑아내는 것처럼, awk를 통해 특정 파일의 레코드들을 읽으며 비슷한 동작을 해줄 수가 있는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;간단한 활용 예시&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 test.txt파일이 있다고 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1713688639214&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;seq_num   name   age   score
1          kim    27     100
2          cho    26      99
3         park    24      94&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;awk를 통해 파일 전체를 출력하려면 다음과 같이 쓴다.&lt;/p&gt;
&lt;pre id=&quot;code_1713688777072&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk '{print}' test.txt
seq_num   name   age   score
1          kim    27     100
2          cho    26      99
3         park    24      94&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;awk를 통해 첫 번째 필드 값만 출력하려면 다음과 같이 쓴다.&lt;/p&gt;
&lt;pre id=&quot;code_1713688850493&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk '{print $1}' test.txt
seq_num
1
2
3&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;awk를 통해 score값(네 번째 필드)이 100점인 사람의 이름만 출력하려면 다음과 같이 쓴다.&lt;/p&gt;
&lt;pre id=&quot;code_1713688932978&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk '$4 == 100 {print $2}' test.txt
kim&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;awk 동작 원리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우선 awk는 입력 데이터(파일을)을 레코드와 필드라는 이름으로 구분한다. RDB 테이블의 행, 열을 떠올리면 이해하기 쉽다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gc2PU/btsGN2bbQqZ/ix6ZfGRAB6eqd9iFZs05Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gc2PU/btsGN2bbQqZ/ix6ZfGRAB6eqd9iFZs05Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gc2PU/btsGN2bbQqZ/ix6ZfGRAB6eqd9iFZs05Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGc2PU%2FbtsGN2bbQqZ%2Fix6ZfGRAB6eqd9iFZs05Dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;291&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;레코드의 텍스트들은 공백 문자(물론 다른 걸로 지정 가능)를 기준으로 각 필드를 구분한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;레코드 하나하나는 $0값으로 들어오며, 해당 레코들의 각 필드들은 $1부터 채워지게 된다(즉 레코드별 첫 필드 = $1).&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 각 레코드와 필드값들을 채웠으면, &quot;pattern(일종의 조건)과 action(실제 수행할 내용)&quot;을 통해 특정 동작들을 수행하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;awk 커맨드 구성은 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713693659145&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk [OPTION] 'pattern { action }' FILE_NAME(or vaiables)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;옵션으로 다음 항목들을 사용 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-F : 필드 구분자를 직접 지정한다&lt;/li&gt;
&lt;li&gt;-f : awk program(awk 명령 스크립트) 이 있는 경로를 지정한다 (즉, 'pattern { action }' 부분은 -f가 없을 때 실행될 'awk program'에 해당&lt;/li&gt;
&lt;li&gt;-v : awk program에서 사용될 특정 변수값들을 지정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;pattern과 action은 둘 다 생략이 가능한 부분이다. pattern이 생략되면 모든 레코드가 적용되고, action이 생략되면 default action인 print가 수행된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713694362082&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# pattern을 생략하는 경우
$ awk '{ print }' test.txt      # test.txt의 모든 레코드를 출력함
seq_num   name   age   score
1          kim    27     100
2          cho    26      99
3         park    24      94

# action을 생략하는 경우
$ awk '$4 &amp;lt; 100' ./file.txt     # test.txt에서 4번째 필드값이 100보다 작은 레코드들 출력함
2          cho    26      99
3         park    24      94&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;pattern&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 봤듯, awk에서 특정 조건에 따라 작업을 처리하게 하는 구성 요소. &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;크게 세 가지 유형으로 나눌 수 있으며, 이들을 조합하거나 단독으로 사용하여 데이터를 필터링하고 조작할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;1. 관계식 패턴&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713694961249&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk '관계식패턴 {action}' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;관계식(비교 연산)을 사용하여 조건에 맞는 라인을 선택하게 한 뒤 action을 수행한다. 필드 값이 특정 조건을 만족하는지 여부를 평가할 때 등에 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713694842828&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ex 1
$ awk '$4 == 100 {print $2}' test.txt
kim

# ex 2
$ awk '$4 &amp;lt; 100' ./file.txt
2          cho    26      99
3         park    24      94&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 정규표현식 패턴&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713695038799&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk '/패턴/ {action}' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;파일에서 pattern(정규표현식을 활용한)이 포함된 모든 라인을 찾아 해당 라인에 대해 &lt;/span&gt;action&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;을 수행한다. 정규표현식은 /들로 둘러싸야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713695206099&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ex 1 : k가 들어있는 레코드만 프린트
$ awk '/k/ {print}' test.txt
1          kim    27     100
3         park    24      94&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. BEGIN &amp;amp; AND 패턴&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713695500864&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk 'BEGIN {action1} pattern {action2} END {action3}' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;특수 패턴으로, BEGIN은 입력 데이터로부터 첫 번째 레코드를 처리하기 전 자신에게 지정된 action을 실행하고, END는 모든 레코드를 처리한 다음 자신에게 지정된 action을 실행한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713695588284&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ex 1
$ awk 'BEGIN {print &quot;Start Processing&quot;} {print $1} END {print &quot;End Processing&quot;}' test.txt
Start Processing
seq_num
1
2
3
End Processing&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;※ &lt;/span&gt;패턴 범위 지정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713695786177&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk 'start_pattern,end_pattern {action}' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;start패턴이 처음 나타난 레코드부터 end패턴이 마지막으로 나타난 라인까지의 모든 레코드들에 대해 action을 수행한다. &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;로그 파일의 특정 섹션을 추출할 때 유용하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;action&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;특정 조건(pattern)에 일치하는 레코드에 대해 수행할 작업을 정의하는 부분이다. &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;중괄호 &lt;/span&gt;{}&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt; 안에 작성되며, 여러 가지 연산과 명령을 포함할 수 있다. 일반적으로 데이터 처리와 조작을 위해 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;편의를 위해, pattern은 생략하고 action들의 유형을 설명한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;1. 출력&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713696676443&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk 'pattern {print}' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴을 만족하는 레코드들을 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713696714529&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ex 1: 4번째 값이 200보다 작은 레코드들을 출력
$ awk '$4 &amp;lt; 200 {print}' test.txt
1          kim    27     100
2          cho    26      99
3         park    24      94&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 변수 할당&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713696799327&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk 'pattern {sum = sum + 1}' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pattern을 만족하는 레코드들에 대해 변수를 할당하는 동작을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713696939540&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ex 1 : 4번째 필드값이 200이하인 모든 레코드의 4번째 필드값을 합산 후 출력
$ awk 'BEGIN {sum_score = 0} $4 &amp;lt; 200 {sum_score = sum_score + $4} END {print sum_score}' test.txt
293&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 조건문 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713697312263&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk 'pattern {if(조건문) action }' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pattern을 만족한 레코드들에 대해 조건문이 true로 평가되면 action을 수행한다. 참고로 else등도 활용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713697494711&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ex 1: k가 포함된 레코드들에 대해 4번째 필드가 100이하면 해당 레코드를 출력한다
$ awk '/k/ {if($4 &amp;lt; 100) print $0}' test.txt
3         park    24      94&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 반복문(for, while) 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713697786056&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ awk 'pattern {반복문 action}' FILE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pattern을 만족한 레코드들에 대해 반복문을 돌며 action을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713697849387&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ex 1 : 4번째 필드가 100보다 작은 레코드들에 대해, 1 ~ NF까지 루프를 돌며
# 현재 레코드 번호와 필드값을 출력
$ awk '$4 &amp;lt; 100 {for(i=1; i&amp;lt;=NF; i++) print NR, $i}' test.txt
3 2
3 cho
3 26
3 99
4 3
4 park
4 24
4 94&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;참고로, NR은 현재 레코드 번호(1부터 시작)를 의미하며 NF는 현재 레코드에 있는 필드값 수를 의미하는 것으로 내장 변수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 파이썬 등과 마찬가지로 break나 continue의 사용이 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도, next라는 action을 통해 다음 레코드로 넘기는 것도 가능하고(일종의 continue), exit를 통해 실행 중이던 awk를 종료시키는 것도 가능하다. 또한 sub, cos같은 다양한 내장 함수도 가지고 있다. 본 글에서처럼 파일에 작성된 값들에 대해 awk를 사용함으로써 데이터 변환 등이 가능하지만, vmstat이나 netstat등의 커맨드 결과를 파이프(|)를 통해 awk의 input으로 줌으로써 모니터링 관리 등에도 활용 가능하다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Linux</category>
      <category>action</category>
      <category>awk</category>
      <category>pattern</category>
      <category>리눅스</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/131</guid>
      <comments>https://jofestudio.tistory.com/131#entry131comment</comments>
      <pubDate>Sun, 21 Apr 2024 20:19:35 +0900</pubDate>
    </item>
    <item>
      <title>Scala Subquery를 LEFT JOIN으로 바꿔 성능 높이기</title>
      <link>https://jofestudio.tistory.com/130</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Subquery란&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SQL 쿼리 내에서 또다른 SELECT절을 사용하는 문법을 쓸 때, &lt;b&gt;내부에 포함되는 SELECT절&lt;/b&gt;을 서브쿼리(Subquery)라 부른다. SELECT 절에서 사용되는 서브쿼리를 &lt;b&gt;스칼라 서브쿼리(Scala Subquery)&lt;/b&gt;, FROM절에서 사용되는 서브쿼리를 &lt;b&gt;인라인뷰(Inline View)&lt;/b&gt;, WHERE절에서 사용되는 서브쿼리를 &lt;b&gt;중첩 서브쿼리(Nested Subquery)&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Scala Subquery란&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SELECT절에서 사용&lt;/b&gt;되는 서브쿼리로, &lt;b&gt;반드시 하나의 record(행)을 리턴해야하며 그 행도 하나의 컬럼값&lt;/b&gt;만 가지고 있어야 한다. (즉 단일행, 단일 컬럼을 반환해야 한다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;예제로, 다음과 같은 두 개의 테이블이 있다고 하자. DEPARTMENTS는 회사에 있는 부서들의 부서id와 부서명을, EMPLOYEES는 임직원들의 이름과 소속 부서id 등을 가진다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;752&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buW3WF/btsFr9ite3H/5JZlA6OfJBzUkeqayh8OP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buW3WF/btsFr9ite3H/5JZlA6OfJBzUkeqayh8OP1/img.png&quot; data-alt=&quot;DEPARTMENTS 테이블&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buW3WF/btsFr9ite3H/5JZlA6OfJBzUkeqayh8OP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuW3WF%2FbtsFr9ite3H%2F5JZlA6OfJBzUkeqayh8OP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;425&quot; height=&quot;261&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DEPARTMENTS 테이블&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;728&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZU35Y/btsFsdkQeET/cBS5Dp0OSI3IpiBqwnsjl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZU35Y/btsFsdkQeET/cBS5Dp0OSI3IpiBqwnsjl1/img.png&quot; data-alt=&quot;EMPLOYEES 테이블&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZU35Y/btsFsdkQeET/cBS5Dp0OSI3IpiBqwnsjl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZU35Y%2FbtsFsdkQeET%2FcBS5Dp0OSI3IpiBqwnsjl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;412&quot; height=&quot;368&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;EMPLOYEES 테이블&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 때, 임직원들의 이름과 그들이 속한 부서의 이름을 함께 조회하고 싶다고 하고, 스칼라 서브쿼리를 사용해본다고 하자. 이때, 스칼라 서브쿼리의 FROM절에서 DEPARTMENTS를 참조하는가, EMPLOYEES를 참조하는가를 참조하는가를 주의해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;스칼라 서브쿼리의 FROM절에서 DEPARTMENTS를 참조할 경우의 쿼리와 결과는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1709455165777&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT EMP_NAME, (
    SELECT DEPT_NAME
    FROM DEPARTMENTS d
    WHERE d.DEPT_ID = e.DEPT_ID) 
FROM 
    EMPLOYEES e;&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;2090&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewofqE/btsFm4WPFRy/oBEMcFafEbAo6oGNFW4Jgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewofqE/btsFm4WPFRy/oBEMcFafEbAo6oGNFW4Jgk/img.png&quot; data-alt=&quot;WooHyung이 가지는 DEPT_ID = 5에 대한 레코드가 DEPARTMENTS테이블에는 없어, 해당 부분은 NULL로 표시됐다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewofqE/btsFm4WPFRy/oBEMcFafEbAo6oGNFW4Jgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FewofqE%2FbtsFm4WPFRy%2FoBEMcFafEbAo6oGNFW4Jgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2090&quot; height=&quot;566&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;WooHyung이 가지는 DEPT_ID = 5에 대한 레코드가 DEPARTMENTS테이블에는 없어, 해당 부분은 NULL로 표시됐다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;스칼라 서브쿼리의 FROM절에서 EMPLOYEES를 참조할 경우의 쿼리와 결과는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1709455231406&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT (
        SELECT EMP_NAME
        FROM EMPLOYEES e
        WHERE d.DEPT_ID = e.DEPT_ID
    ), DEPT_NAME 
FROM 
    DEPARTMENTS d;&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;1216&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn6o0e/btsFocAiUAi/DzeyTutX6jJNFjUdzjqXlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn6o0e/btsFocAiUAi/DzeyTutX6jJNFjUdzjqXlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn6o0e/btsFocAiUAi/DzeyTutX6jJNFjUdzjqXlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn6o0e%2FbtsFocAiUAi%2FDzeyTutX6jJNFjUdzjqXlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;743&quot; height=&quot;38&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;서브쿼리의 FROM절에서 DEPARTMENTS를 참조하는 경우, d.DEPT_ID = e.DEPT_ID를 만족하는 레코드가 하나만 나오기 때문에 문제가 되지 않는다. 하지만 서브쿼리의 FROM절에서 EMPLOYEES를 참조하는 경우, d.DEPT_ID = e.DEPT_ID를 만족하는 레코드가 다수가 나오기 때문에 문제가 되고, 위와 같은 오류가 생기는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;스칼라 서브쿼리는 반드시 하나의 레코드와 하나의 컬럼값을 리턴&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스칼라 서브쿼리의&amp;nbsp; 성능 측면에서의 문제점 &amp;amp; LEFT OUTER JOIN으로 변환&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리는 &lt;b&gt;FROM(과 JOIN) &amp;rarr; WHERE &amp;rarr; GROUP BY &amp;rarr; HAVING &amp;rarr; SELECT &amp;rarr; ORDER BY 순으로 실행&lt;/b&gt;된다. 즉 특정 테이블에서 특정 조건에 맞는 레코들들을 모두 뽑아오고, 해당 레코드들에 대해 select절에서 특정 컬럼들을 뽑아내는 식으로 동작한다.   &lt;b&gt;스칼라 서브쿼리는 건수만큼 반복해서 수행&lt;/b&gt;된다. 데이터가 많아질수록 성능 저하의 주범이 될 수 있는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;따라서, &lt;b&gt;LEFT OUTER JOIN과 Inline View를 활용하는 식&lt;/b&gt;으로 쿼리를 바꿔 성능을 개선시킬 수 있다. 다음 쿼리를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1709556637204&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DEPT_NAME, (
    SELECT COUNT(*)
    FROM EMPLOYEES e
    WHERE e.DEPT_ID = d.DEPT_ID
) NUMS
FROM DEPARTMENTS d;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;부서별로 부서명과 소속 인원들의 이름을 보는, 스칼라 서브쿼리를 쓰는 쿼리다. 이를 LEFT OUTER JOIN과 Inline View을 활용하는 형태로 다음과 같이 바꿔쓸 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1709557345843&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DEPT_NAME, c.NUMS
FROM DEPARTMENTS d
LEFT OUTER JOIN (
    SELECT e.DEPT_ID, COUNT(*) AS NUMS
    FROM EMPLOYEES e
    GROUP BY e.DEPT_ID
) c
ON d.DEPT_ID = c.DEPT_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 해당 서브쿼리가 건수만큼 반복돼서 수행됐지만, 이렇게하면 한 번만 수행되고 끝난다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 늘상 스칼라 서브쿼리를 LEFT OUTER JOIN으로 바꾼다고 성능 향상이 되는 건 아니다(그러면 스칼라 서브쿼리 아무도 안 쓰지..). 상황별로 스칼라 서브쿼리를 쓰는게 이득일 때도 있다. 다만 스칼라 서브쿼리를 쓸 때는 속도를 고려하는 습관을 가지자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB</category>
      <category>Left Outer Join</category>
      <category>Scala</category>
      <category>subquery</category>
      <category>서브쿼리</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/130</guid>
      <comments>https://jofestudio.tistory.com/130#entry130comment</comments>
      <pubDate>Mon, 4 Mar 2024 22:25:29 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] WITH ROLLUP을 활용한 그룹별 중간 집계 산출법</title>
      <link>https://jofestudio.tistory.com/129</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 테이블이 있다고 하자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SALES_HISTORY 테이블은 특정 상품들과 해당 상품들이 판매된 시간, 판매된 가격 정보를 가진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDdQTY/btsFoITdYyK/34Hm601BmuWqXgYzkQ1s9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDdQTY/btsFoITdYyK/34Hm601BmuWqXgYzkQ1s9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDdQTY/btsFoITdYyK/34Hm601BmuWqXgYzkQ1s9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDdQTY%2FbtsFoITdYyK%2F34Hm601BmuWqXgYzkQ1s9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;758&quot; height=&quot;208&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. GROUP BY를 통한 집계&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;GROUP BY를 통해 특정 컬럼(들)을 기준으로 그룹핑한 다음 집계함수를 활용한 합이나 평균값의 산출이 가능하다. 본 글에서는 SUM을 활용한 상품들의 일자별 매출액을 조회하는 상황을 가정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1709447555604&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
  PRODUCT_ID,
  DATE_FORMAT(SALES_DATE, '%Y-%m-%d') AS SALES_DAY,
  SUM(PRICE) AS TOTAL_SALES
FROM
  SALES_HISTORY
GROUP BY
  PRODUCT_ID, SALES_DAY;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;922&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OCh7H/btsFoH05pmV/kGk9U7dJqfj6G1AlcfHxn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OCh7H/btsFoH05pmV/kGk9U7dJqfj6G1AlcfHxn1/img.png&quot; data-alt=&quot;각 상품들의 일자별 매출액을 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OCh7H/btsFoH05pmV/kGk9U7dJqfj6G1AlcfHxn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOCh7H%2FbtsFoH05pmV%2FkGk9U7dJqfj6G1AlcfHxn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;438&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;각 상품들의 일자별 매출액을 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. WITH ROLLUP을 가미한 집계&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;WITH ROLLUP을 가미하면, GROUP BY로 보여지는 결과물 뿐만 아니라 그룹핑한 각각의 소그룹별 집계와 전체 집계를 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1709448162401&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
  PRODUCT_ID,
  DATE_FORMAT(SALES_DATE, '%Y-%m-%d') AS SALES_DAY,
  SUM(PRICE) AS TOTAL_SALES
FROM
  SALES_HISTORY
GROUP BY
  PRODUCT_ID, SALES_DAY WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-03-03 오후 3.43.18.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IBOQr/btsFwCRGoHR/RKg1UIWp1Cs1xPwl7ogYzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IBOQr/btsFwCRGoHR/RKg1UIWp1Cs1xPwl7ogYzk/img.png&quot; data-alt=&quot;각 상품들의 일자별 매출액과 더불어 상품별 매출액, 전체 매출액까지 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IBOQr/btsFwCRGoHR/RKg1UIWp1Cs1xPwl7ogYzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIBOQr%2FbtsFwCRGoHR%2FRKg1UIWp1Cs1xPwl7ogYzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;593&quot; height=&quot;385&quot; data-filename=&quot;스크린샷 2024-03-03 오후 3.43.18.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;각 상품들의 일자별 매출액과 더불어 상품별 매출액, 전체 매출액까지 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;NULL값으로 표기된 부분들이 WITH ROLLUP을 통해 산출된 집계값이다. 1000번 상품 그룹에 대한 집계와 1001번 상품에 대한 집계, 1002번 상품에 대한 집계와 더불어 전체 집계 또한 나온 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MySQL</category>
      <category>GROUP BY</category>
      <category>MySQL</category>
      <category>with rollup</category>
      <category>집계</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/129</guid>
      <comments>https://jofestudio.tistory.com/129#entry129comment</comments>
      <pubDate>Sun, 3 Mar 2024 16:14:44 +0900</pubDate>
    </item>
    <item>
      <title>SQL Mapper VS ORM (MyBatis vs JPA)</title>
      <link>https://jofestudio.tistory.com/128</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Persistence와 Persistence Framework&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;영속성은 프로그램이 종료되도 데이터가 보존되는 것을 말한다. 자바에서는 JDBC(Java Database Connectivity)라 불리는 기술을 통해 메모리에 있는 객체들에게 Persistence를 줄 수 있다(즉 데이터로 저장할 수 있다는 말).&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1422&quot; data-origin-height=&quot;1664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1nRWE/btsFkUGzQp0/mY7IzKQcR3d0E4nm4Hwgf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1nRWE/btsFkUGzQp0/mY7IzKQcR3d0E4nm4Hwgf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1nRWE/btsFkUGzQp0/mY7IzKQcR3d0E4nm4Hwgf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1nRWE%2FbtsFkUGzQp0%2FmY7IzKQcR3d0E4nm4Hwgf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;791&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;1664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;자바 앱에서 JDBC API를 호출하고, 내부적으로 JDBC driver manager를 이용해 DB와 상호작용하는 것을 도식화한 그림이다. 참고로 MySQL, SingleStore 등 저마다의 DBMS들은 자신들에 맞는 JDBC driver를 제공한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러나, 이렇게 JDBC만을 사용하는 것은 매번 JDBC로 DB와 커넥션을 맺고, 쿼리를 날려 수행하고, 결과를 받은 다음 커넥션을 끊는 작업을 프로그래밍해야 한다는 문제가 있었다. 이런 과정을 단순화하여 간단하게 DB와 상호작용하기 위해, 내부적으로 JDBC API를 활용하게끔 하여 개발자의 수고를 덜어주는 Persistence Framework가 등장했으며 대표적으로 SQL Mapper와 ORM이 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SQL Mapper&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oL9bm/btsFjI0NzAY/fkiTw8U5EGkBD2YPmygcak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oL9bm/btsFjI0NzAY/fkiTw8U5EGkBD2YPmygcak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oL9bm/btsFjI0NzAY/fkiTw8U5EGkBD2YPmygcak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoL9bm%2FbtsFjI0NzAY%2FfkiTw8U5EGkBD2YPmygcak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2076&quot; height=&quot;402&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;개발자가 직접 작성한 SQL 쿼리의 &lt;b&gt;실행 결과를 객체로&lt;/b&gt; 바인딩해주는 기술. 대표적으로는 MyBatis가 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SQL에 의존성이 높은 방법으로, MyBatis의 경우 xml파일을 통해 SQL 쿼리를 별도 관리하며 동적 쿼리 작성(실행 중 사용자 입력을 통해 들어오는 파라미터 값에 따라 다른 쿼리가 실행되게 하는 것)을 통해 복잡한 쿼리도 처리할 수 있다는 장점이 있다. DBA같은 밥먹고 DB만 파오신 전문가 분들께 복잡한 쿼리 작성을 짬때리는게 가능해진다(?)는 장점도 있다. 그러나 어찌됐건 개발자가 직접 SQL을 작성해야 한다는 것에서 결국은 공수가 드는 것이며, 데이터 모델 패러다임 불일치(자바는 클래스와 객체를 통해 데이터를 모델링하나 DB는 테이블과 컬럼을 통해 데이터를 모델링한다는 것에서 오는 차이)가 발생한다는 단점이 있다. 즉, 객체지향적인 관점에서의 프로그래밍이 어렵다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ORM (Object Relation Mapping)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chx65P/btsFpRuYBCC/2knpoEJZgFSukVvDATA8lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chx65P/btsFpRuYBCC/2knpoEJZgFSukVvDATA8lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chx65P/btsFpRuYBCC/2knpoEJZgFSukVvDATA8lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchx65P%2FbtsFpRuYBCC%2F2knpoEJZgFSukVvDATA8lk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2042&quot; height=&quot;432&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;객체와 DB의 데이터&lt;/b&gt;를 자동으로 매핑해주는 기술. 대표적으로는 JPA가 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;설정된 관계를 기반으로 자동으로 SQL 쿼리가 생성되며, DBMS에 의존적이지 않아 개발자가 비즈니스 로직에 좀 더 집중할 수 있는 효과를 줄 수 있다. 또한 DB 데이터를 객체로 매핑해주기 때문에 좀 더 객체지향적인 관점에서의 프로그래밍이 용이하다. 자체적으로  복잡한 쿼리의 자동생성은 어렵기 때문에 OLAP성 업무보다는 OLTP성 업무가 많이 쓰일 때 JPA를 활용하면 좋다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 JPA는 자바 진영에서 제공하는 ORM을 위한 표준 기술로 그 자체는 &quot;&lt;b&gt;인터페이스&quot;&lt;/b&gt;다. 즉, JPA를 사용하려면 해당 인터페이스를 구현한 구현체를 별도로 사용해야 하며, 대표적으로 &lt;b&gt;Hibernate&lt;/b&gt;가 가장 유명하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/Spring</category>
      <category>java</category>
      <category>JPA</category>
      <category>MyBatis</category>
      <category>ORM</category>
      <category>persistence</category>
      <category>SQL Mapper</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/128</guid>
      <comments>https://jofestudio.tistory.com/128#entry128comment</comments>
      <pubDate>Wed, 28 Feb 2024 23:13:58 +0900</pubDate>
    </item>
    <item>
      <title>MySQL에서 데이터를 scan하는 대표적인 방법들 (feat. InnoDB 클러스터링 인덱스)</title>
      <link>https://jofestudio.tistory.com/127</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MySQL, 그리고 InnoDB의 Index&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스(Index)는 DB에서 테이블의 데이터를 빠르게 검색하기 위해 사용되는 보조적인 자료구조로, 인덱스와 실제 데이터는 따로 보관 및 관리된다. 전통적으로 인덱스를 위해 가장 많이 사용되는 자료구조는 B-tree (Balanced Tree)로, Leaf Node는 실제 데이터 레코드를 찾아갈 수 있는 주솟값을 가지고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1858&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blvqTg/btsEZQcs3Jd/mR4jRnRpcsq2FBbmnqfzYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blvqTg/btsEZQcs3Jd/mR4jRnRpcsq2FBbmnqfzYk/img.png&quot; data-alt=&quot;B+ tree&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blvqTg/btsEZQcs3Jd/mR4jRnRpcsq2FBbmnqfzYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblvqTg%2FbtsEZQcs3Jd%2FmR4jRnRpcsq2FBbmnqfzYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1858&quot; height=&quot;1014&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;1014&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;B+ tree&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL 역시 B-tree 기반 인덱스를 사용하며, InnoDB 엔진의 경우 클러스터링 인덱스(클러스터링 테이블이라고도 부름)를 사용하는데 이는 테이블에서 PK(Primary Key)값이 비슷한 레코드들끼리 묶어 저장하는 걸 의미한다. 즉 스토리지 엔진으로 InnoDB를 사용한다면 아무 것도 하지 않아도 알아서 PK값들에 의해 B-tree 구조로 테이블의 데이터가 저장된다(각 Leaf Node가 레코드의 모든 컬럼 값을 가짐). 즉&lt;span&gt;&amp;nbsp;&lt;/span&gt;PK값에 따라 레코드의 저장 위치가 결정되는 것이며, 만약 PK값이 변경된다면 해당 레코드가 저장되는 물리적 위치 또한 바뀌게 됨을 의미한다. 따라서 이렇게 클러스터링 인덱스로 저장되는 테이블은 PK 기반의 검색이 매우 빠르나 레코드의 저장이나 PK 변경은 상대적으로 느리다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2630&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBfHXj/btsE1DKrnx9/T2Oen5yV8Q1vqtMVi359pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBfHXj/btsE1DKrnx9/T2Oen5yV8Q1vqtMVi359pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBfHXj/btsE1DKrnx9/T2Oen5yV8Q1vqtMVi359pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBfHXj%2FbtsE1DKrnx9%2FT2Oen5yV8Q1vqtMVi359pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2630&quot; height=&quot;1040&quot; data-origin-width=&quot;2630&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 특성 때문에, 세컨더리 인덱스에 대한 B-tree를 기준으로 볼 때 스토리지 엔진으로 MyISAM을 사용할 경우 Leaf Node가 실제 레코드의 물리적인 주소들을 가지는 반면 InnoDB 는 Leaf Node가 PK를 가진다. 즉 InnoDB를 사용할 경우 세컨더리 인덱스를 통해 데이터를 읽을 때 데이터를 바로 찾아가는 게 아니라, Leaf Node에 저장된 PK를 이용해 클러스터링 인덱스에서 찾아가게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;본 포스트에서는 Index를 사용하지 않는 Full Table Scan방법과 Index를 사용하는 4가지 Scan 방법에 대해 다룬다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Full Table Scan (=Table Full Scan)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;말 그대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;테이블 전체 데이터를 순차적으로 scan하는 방식&lt;/b&gt;이다(배열에서 모든 데이터를 선형탐색으로 읽는 것에 비유할 수 있다). InnoDB가 데이터(인덱스도 포함)들을 저장하는 기본적인 단위는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;페이지&quot;&lt;/b&gt;인데, Full Table Scan은 테이블의 데이터가 담긴 페이지들을 모두 읽어와서 읽게 된다. 레코드가 많은 테이블의 경우, Full Table Scan은 성능 저하의 주범이 되는 경우가 많다. 일반적으로 인덱스가 없거나, 또는 인덱스를 활용할 수 없거나, 아니면 테이블의 레코드가 너무 적어서 인덱스를 사용하지 않고 테이블 전체를 읽는 게 더 빠를 경우 이 방법이 사용된다.&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;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kOvBP/btsE0IrzdXT/AWSrH1DEeUOHgwIkUZwKFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kOvBP/btsE0IrzdXT/AWSrH1DEeUOHgwIkUZwKFk/img.png&quot; data-alt=&quot;출처 : 실무에 바로 쓰는 SQL 튜닝&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kOvBP/btsE0IrzdXT/AWSrH1DEeUOHgwIkUZwKFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkOvBP%2FbtsE0IrzdXT%2FAWSrH1DEeUOHgwIkUZwKFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;356&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 실무에 바로 쓰는 SQL 튜닝&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Index Full Scan&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스의 Leaf 노드 전체(즉 인덱스의 처음부터 끝까지)를 순차적으로 scan하지만 실제 테이블을 참조하지는 않는 방식&lt;/b&gt;을 말한다. 쿼리가 인덱스에 명시된 컬럼들만으로 조건을 처리가능한 경우에 주로 이 방법이 사용된다.(B-tree의 각 노드가 인덱스로 명시된 컬럼값과 child node의 위치를 키-값 형태로 가지고 있음을 상기할 것) 인덱스 뿐만이 아닌 레코드까지 읽어야 한다면 절대 이 방식을 사용하지 않으며, 실제 테이블을 참조하지는 않으니 Full Table Scan보다는 효율적(왜냐면 인덱스의 전체 크기가 테이블 자체의 사이즈보다 작아서 Disk I/O가 줄어들기 때문)이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 이렇게 쿼리가 인덱스에 존재하는 컬럼만으로 처리 가능할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;커버링 인덱스&quot;&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1514&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ztJV1/btsE0hgGFHf/KxF0HKkn02iJ7LKzU0f4Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ztJV1/btsE0hgGFHf/KxF0HKkn02iJ7LKzU0f4Bk/img.png&quot; data-alt=&quot;출처 : 실무에 바로 쓰는 SQL 튜닝&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ztJV1/btsE0hgGFHf/KxF0HKkn02iJ7LKzU0f4Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FztJV1%2FbtsE0hgGFHf%2FKxF0HKkn02iJ7LKzU0f4Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;508&quot; height=&quot;309&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 실무에 바로 쓰는 SQL 튜닝&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Index Range Scan&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정 범위만큼의 인덱스를 스캔한 뒤 이에 대응하는 레코드들을 읽는 방식&lt;/b&gt;이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿼리에서 BETWEEN이나 부등호 등이 사용되어 검색해야 할 인덱스의 범위(range)가 결정됐을 때 사용된다. 스캔의 시작점이 되는 리프 노드만 찾으면 그때부터는 종료 지점까지의 리프 노드들을 쫙 스캔한 후(B+ tree는 리프 노드끼리도 서로 연결돼있음을 상기), 필요하다면 읽은 리프 노드들이 가리키는 레코드들을 읽어온다(따라서 Random I/O가 수반되게 된다). 쿼리가 인덱스에 명시된 컬럼들만으로 조건을 처리가능하다면(즉 커버링 인덱스) Index Full Scan처럼 테이블 레코드들을 읽어오는 과정은 없다.&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;1532&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cixL5u/btsEWzQDBq5/401a1PmhbUTsO6ywZ40XF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cixL5u/btsEWzQDBq5/401a1PmhbUTsO6ywZ40XF0/img.png&quot; data-alt=&quot;출처 : 실무에 바로 쓰는 SQL 튜닝&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cixL5u/btsEWzQDBq5/401a1PmhbUTsO6ywZ40XF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcixL5u%2FbtsEWzQDBq5%2F401a1PmhbUTsO6ywZ40XF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;517&quot; height=&quot;300&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 실무에 바로 쓰는 SQL 튜닝&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Loose Index Scan&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스에서 필요한 부분만 선택적으로 스캔한 뒤 이에 대응하는 레코드들을 읽는 방식&lt;/b&gt;이다. Index Range Scan과 비슷하게 동작하나, 필요한 인덱스 키와 불필요한 인덱스 키를 구분한 뒤 불필요한 인덱스 키는 무시하는 식으로 동작한다. 보통 GROUP BY, MAX, MIN 등이 사용된 쿼리를 최적화할 때 사용된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1546&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buhWSf/btsE2Blo8cp/yxl5kZ6PVUHNa7rWZTsoK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buhWSf/btsE2Blo8cp/yxl5kZ6PVUHNa7rWZTsoK1/img.png&quot; data-alt=&quot;출처 : 실무에 바로 쓰는 SQL 튜닝&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buhWSf/btsE2Blo8cp/yxl5kZ6PVUHNa7rWZTsoK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuhWSf%2FbtsE2Blo8cp%2Fyxl5kZ6PVUHNa7rWZTsoK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;542&quot; height=&quot;323&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 실무에 바로 쓰는 SQL 튜닝&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Index Skip Scan&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 두 개 이상의 컬럼에 대해서도 만들어줄 수 있는데(멀티 컬럼 인덱스 등으로 부름), 이 때 컬럼 순서에 따라 인덱스가 어떤 기준으로 정렬되는지가 결정된다.(예를 들어 A, B 순서로 인덱스를 만들었다면 A에 대해 먼저 정렬된 다음 B에 대해 정렬됨) 이때 WHERE절에 멀티 컬럼 인덱스의 첫 컬럼이 없으면 원래 인덱스를 사용할 수 없으나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;첫 컬럼이 없음에도 불구하고 인덱스를 사용할 수 있는 스캔 방식&lt;/b&gt;이 이 Index Skip Scan이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;예로,&amp;nbsp;student테이블에 대해 다음과 같이 gender, address에 대해 인덱스가 걸려있다고 해보자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708187222736&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE students ADD INDEX ix_gender_address (gender, address);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이때다음 쿼리는 원래는 Full Table Scan 또는 Index Full Scan(커버링 인덱스인 경우)으로 수행될 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708187222736&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT gender, address
FROM students
WHERE address LIKE &quot;경기도%&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Index Skip Scan을 사용하면 WHERE절에서 쓰이지 않은 인덱스의 선행 컬럼에 대해 가능한 값들을 구한 다음, 해당 컬럼의 조건을 추가해 쿼리를 다시 실행하는 형태로 처리하게 된다. 즉 여기서는 WHERE절에서 쓰이지 않은 gender라는 컬럼에 대해, 해당 컬럼에서 유니크한 값들을 모두 뽑은 다음 다음과 같은 형태의 쿼리를 다시 실행하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708187222737&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT gender, address
FROM students
WHERE gender = &quot;M&quot; AND address LIKE &quot;경기도%&quot;;

SELECT gender, address
FROM students
WHERE gender = &quot;F&quot; AND address LIKE &quot;경기도%&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로, Index Skip Scan은 WHERE절에서 쓰이지 않는 선행컬럼의 유니크한 값의 개수가 적어야 하고(많으면 오히려 쿼리를 처리하는 속도가 더 느려질 수 있기 때문), 커버링 인덱스를 만족해야 한다는 단점이 있다.&lt;/p&gt;</description>
      <category>DB/MySQL</category>
      <category>Index</category>
      <category>InnoDB</category>
      <category>MySQL</category>
      <category>scan</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/127</guid>
      <comments>https://jofestudio.tistory.com/127#entry127comment</comments>
      <pubDate>Sun, 18 Feb 2024 01:27:31 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 실행 계획이란</title>
      <link>https://jofestudio.tistory.com/126</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;쿼리 실행 계획(Query Plan)이란&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;말 그대로 &lt;b&gt;DBMS가 주어진 SQL 쿼리를 어떻게 처리하겠다고 짜는 계획&lt;/b&gt;을 말한다. 같은 쿼리는 같은 결과를 내뱉지만, 내부적으로 그 결과를 만들어내는 방법은 다양하다. 그 결과를 만들기 위한 여러 Plan 중, 가장 최소한의 비용이 드는 Plan을 수립해야 한다. MySQL을 비롯한 대부분의 DBMS에서는 &lt;b&gt;&quot;옵티마이저&quot;&lt;/b&gt;가 이러한 역할을 담당한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;옵티마이저(Optimizer)란&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;가장 효율적인 방법으로 SQL을 수행할 최적의 경로, 즉 최고의 플랜을 생성해주는 DBMS의 핵심 엔진을 말한다. DB 서버에서의 두뇌 역할을 담당한다고 볼 수 있으며, 다음 두 가지 종류로 나눌 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;/li&gt;
&lt;li&gt;비용 기반 최적화 : 쿼리를 처리 가능한 여러 방법을 만들고, 각 단위 작업의 비용 정보와 통계 정보(테이블 레코드 수, 컬럼 내부 NULL값 분포도, 테이블아 차지하는 블록 개수 등..)를 이용해 실행 계획별 비용을 산출한 후, 가장 최소의 비용이 드는 실행 계획을 선택하는 옵티마이저를 말한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실행 계획 보는 방법 (MySQL 기준)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;가장 대표적인 방법은 &lt;b&gt;&quot;EXPLAIN&quot;&lt;/b&gt;을 사용하는 것이다. 다음과 같이 실행 계획을 확인하고 싶은 쿼리의 앞에 EXPLAIN만 붙여주면 된다. 사용된 쿼리는 고객별로 Meatlovers 또는 Vegetarian이라는 이름의 피자 당 주문한 갯수를 조회하는 쿼리이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708083224775&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
SELECT customer_id, pizza_name, count(*) as order_count
FROM customer_orders
INNER JOIN pizza_names ON customer_orders.pizza_id = pizza_names.pizza_id
WHERE pizza_name in (&quot;Meatlovers&quot;, &quot;Vegetarian&quot;)
GROUP BY customer_id, pizza_name;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;3554&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NDCKg/btsEWAuY0RT/RR06F6mtSlH4pMVcKDok60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NDCKg/btsEWAuY0RT/RR06F6mtSlH4pMVcKDok60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NDCKg/btsEWAuY0RT/RR06F6mtSlH4pMVcKDok60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNDCKg%2FbtsEWAuY0RT%2FRR06F6mtSlH4pMVcKDok60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3554&quot; height=&quot;302&quot; data-origin-width=&quot;3554&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;EXPLAIN FORMAT&quot;&lt;/b&gt;을 통해 트리(또는 json( 형태로 실행 계획을 볼 수도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708084273963&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN FORMAT=TREE
SELECT customer_id, pizza_name, count(*) as order_count
FROM customer_orders
INNER JOIN pizza_names ON customer_orders.pizza_id = pizza_names.pizza_id
WHERE pizza_name in (&quot;Meatlovers&quot;, &quot;Vegetarian&quot;)
GROUP BY customer_id, pizza_name;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;3446&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nSxbC/btsE0peiFyM/knmNQCuMoejURajd2Uel71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nSxbC/btsE0peiFyM/knmNQCuMoejURajd2Uel71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nSxbC/btsE0peiFyM/knmNQCuMoejURajd2Uel71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnSxbC%2FbtsE0peiFyM%2FknmNQCuMoejURajd2Uel71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3446&quot; height=&quot;710&quot; data-origin-width=&quot;3446&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;EXPLAIN ANALYZE&quot;&lt;/b&gt;를 사용하면 실행 계획 뿐만 아니라 단계별 소요된 시간 정보도 확인할 수 있다. &lt;b&gt;즉, EXPLAIN은 실행 계획을 만들기만 하고 실제 쿼리 실행은 하지 않는 반면, EXPLAIN ANALYZE는 실제 쿼리를 실행하고 사용된 실행 계획과 소요된 시간을 보여주는 것&lt;/b&gt;이다. 참고로 이 커맨드는 항상 트리 형태로 실행 계획을 보여준다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;3456&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vEcQe/btsEVJspmpM/CC1UBKC6bijuL9un49AQcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vEcQe/btsEVJspmpM/CC1UBKC6bijuL9un49AQcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vEcQe/btsEVJspmpM/CC1UBKC6bijuL9un49AQcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvEcQe%2FbtsEVJspmpM%2FCC1UBKC6bijuL9un49AQcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3456&quot; height=&quot;900&quot; data-origin-width=&quot;3456&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;트리 형태를 기준으로, 실제 실행 순서는 위에서 아래로 다음과 같이 읽으면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;들여쓰기(indent)가 같으면, 위쪽에 있는 계획이 먼저 실행된다&lt;/li&gt;
&lt;li&gt;들여쓰기(indent)가 다르면, 가장 안쪽에 있는 계획이 먼저 실행된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;즉 위 실행 계획은 다음 순서로 실행된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Table scan on pizza_names&lt;/li&gt;
&lt;li&gt;Filter: (pizza_names.pizza_name in ('Meatlovers', 'Vegetarian'))&lt;/li&gt;
&lt;li&gt;Table scan on customer_orders&lt;/li&gt;
&lt;li&gt;Hash&lt;/li&gt;
&lt;li&gt;Inner&amp;nbsp;hash&amp;nbsp;join&amp;nbsp;(customer_orders.pizza_id&amp;nbsp;=&amp;nbsp;pizza_names.pizza_id)&lt;/li&gt;
&lt;li&gt;Aggregate&amp;nbsp;using&amp;nbsp;temporary&amp;nbsp;table&lt;/li&gt;
&lt;li&gt;Table&amp;nbsp;scan&amp;nbsp;on&amp;nbsp;&amp;lt;temporary&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 해석해보면,&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;pizza_names 테이블을 쭉 읽으면서&lt;/li&gt;
&lt;li&gt;pizza_name칼럼값이 'Meatlovers' 또는 'Vegetarian'인 레코드만 가져온다&lt;/li&gt;
&lt;li&gt;그 다음엔 customer_orders테이블을 쭉 읽으면서&lt;/li&gt;
&lt;li&gt;Hash 테이블을 정한 뒤&lt;/li&gt;
&lt;li&gt;customer_orders와 pizza_names 간 해시 조인을 수행한다&lt;/li&gt;
&lt;li&gt;임시 테이블에 결과를 저장하며 GROUP BY 집계를 수행하고&lt;/li&gt;
&lt;li&gt;임시 테이블 결과를 읽어서 반환한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 실행 계획의 각 스텝마다 actual time이 2개씩 적혀있는 걸 확인할 수 있는데, 첫 번째 값은 첫 번째 레코드를 가져오는데 걸린 평균 시간(밀리세컨드)을 의미하고 두 번째 값은 마지막 레코드를 가져오는데 걸린 평균 시간(밀리세컨드)를 의미한다. 또한 rows는 해당 테이블에서 읽은 테이블의 평균 레코드 수를 의미하고, loops는 해당 테이블의 레코드를 찾는 작업이 반복된 횟수를 의미한다. (이 loop 때문에 레코드를 가져오는 &lt;b&gt;&quot;평균&quot;&lt;/b&gt; 시간, 테이블의 &lt;b&gt;&quot;평균&quot;&lt;/b&gt; 레코드 수라고 하는 것이다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB</category>
      <category>DB</category>
      <category>MySQL</category>
      <category>실행계획</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/126</guid>
      <comments>https://jofestudio.tistory.com/126#entry126comment</comments>
      <pubDate>Fri, 16 Feb 2024 21:35:27 +0900</pubDate>
    </item>
    <item>
      <title>docker-compose 훑어보기</title>
      <link>https://jofestudio.tistory.com/125</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도커 컴포즈(Docker-Compose)란&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면, &lt;b&gt;&quot;여러 컨테이너를 하나의 서비스로 묶어주는 관리 환경을 제공해주는 도구&quot;&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;도커는 웹 서버 - WAS - DB 등과 같은 3 tier service부터 MSA 등 &lt;b&gt;분산된 컴포넌트를 실행 가능한 환경&lt;/b&gt;을 가지고 있으며, 각 컴포넌트들이 자신만의 컨테이너에서 실행되면서 도커가 &lt;b&gt;표준 네트워크 프로토콜&lt;/b&gt;을 통해 이들을 엮어낸다. 도커 컴포즈를 통해 이렇게 여러 컨테이너를 통해 돌아가는 애플리케이션을 정의하고 관리할 수 있게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;한 마디로 도커 컴포즈가 없다면 애플리케이션을 구성하는 컨테이너를 직접 하나하나 docker run을 때려가며 구동시켜야 했지만, 도커 컴포즈라는 툴 통해 컨테이너들을 하나로 묶어서 관리할 수 있다는 것.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 말했듯, 도커 컴포즈는 도커에서 기본적으로 제공하는 &lt;b&gt;&quot;기능&quot;&lt;/b&gt;이 아닌 여러 컨테이너를 엮고 관리하게 해주는 &lt;b&gt;&quot;툴&quot;&lt;/b&gt;이기 때문에 별도의 설치가 필요하다. 도커 컴포즈는 docker-compose.yml파일을 작성한 후, docker-compose up 명령어를 통해 실행 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;docker-compose.yml&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;되새김질 차원에서 기존에 docker run으로 컨테이너를 돌리던 커맨드를 봐보면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1707310845358&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run --name mongodb -v ~/data:/data/db -d -p 27017:27017 mongo&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;--name mongodb : mongodb라는 컨테이너명으로 돌리겠다&lt;/li&gt;
&lt;li&gt;-v ~/data:/data/db : 호스트의 ~/data를 실행될 컨테이너 내부의 /data/db로 마운트하겠다&lt;/li&gt;
&lt;li&gt;-d : 컨테이너를 백그라운드에서 실행하겠다(detached mode)&lt;/li&gt;
&lt;li&gt;-p 27017:27017 : 호스트의 27017 포트를 컨테이너의 27017포트로 포워딩하겠다&lt;/li&gt;
&lt;li&gt;mongo : mongo라는 docker image를 사용하겠다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이를 docker-compose.yml파일로 옮겨 적으면 다음과 같이 적을 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1707311112497&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'

services:
  mongodb:
    image: mongo
    volumes:
      - ~/data:/data/db
    ports:
      - &quot;27017:27017&quot;
    restart: always&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;본 예제에서는 컨테이너를 하나만 적어주긴 했으나, 보다시피 도커 컴포즈 파일은 애플리케이션의 모든 컴포넌트가 실행되고 있을 때 &lt;b&gt;어떤 상태여야 하는지&lt;/b&gt;를 기술하는 파일이다. docker run으로 컨테이너를 돌릴 때 사용하던 옵션들을 한 곳에 모아둔 단순한 형식이며, version에 도커 컴포즈 파일 형식의 버전을 작성하고, services 아래로 &lt;b&gt;엮어줄 컨테이너들의 정보를 작성하는 형태&lt;/b&gt;이다(위 예시에선 몽고디비 컨테이너만 기재했으나, 당연히 다른 컨테이너들도 쭈루룩 쓸 수 있는 것이다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;재밌는 점은, 도커 컴포즈는 이렇게 단순히 컴포넌트들을 동시 실행하도록 해주는 것 뿐만 아니라 컨테이너 간의 &lt;b&gt;의존관계도 정해줄 수 있다는 것&lt;/b&gt;이다. 다음 docker-compose.yml파일을 보자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707314625638&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'

services:
  mongodb:
    image: mongo
    volumes:
      - ~/data:/data/db
    ports:
      - &quot;27017:27017&quot;
    restart: always
    
  ray:
    image: ray/ray-ray-image
    ports:
      - &quot;8010:80&quot;
    depends_on:
      - mongodb&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;docker-compose는 컨테이너를 구동할 때 docker-compose.yml에 작성된 순서대로 컨테이너를 돌리지 않고, &lt;b&gt;병렬로 즉 동시에 실행&lt;/b&gt;시키려고 한다. 이 때 위 예시처럼 depends_on을 활용하여 컨테이너 간 의존성을 명시해 컨테이너가 구동되는 순서를 맞춰줄 수 있다. ray컴포넌트는 mondodb컨테이너가 구동된 뒤 실행되게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS</category>
      <category>docker</category>
      <category>docker-compose</category>
      <category>도커 컴포즈</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/125</guid>
      <comments>https://jofestudio.tistory.com/125#entry125comment</comments>
      <pubDate>Wed, 7 Feb 2024 23:26:52 +0900</pubDate>
    </item>
    <item>
      <title>[DB ]rowstore vs columnstore (row oriented db vs column oriented db)</title>
      <link>https://jofestudio.tistory.com/124</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 테이블이 있다고 하자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 68px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;gender&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;age&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;address&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Jho&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;male&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;27&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;seoul&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Kim&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;male&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;29&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;busan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Park&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;female&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;30&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;suwon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;rowstore&lt;/b&gt;는 다음과 같이 데이터를 행 단위로 저장하는 것을 말한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;Jho&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;male&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;27&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;seoul&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;Kim&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;male&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;29&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;busan&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;Park&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;female&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;insert, update, delete를 수행할 경우 row단위로 묶어서 빠르게 처리를 해줄 수 있어서 효율적이다. 반면 모든 사람의 평균 나이처럼 특정 컬럼에 대한 집계가 필요한 경우는 필요없는 컬럼에 대한 read도 하게 되므로 비효율적이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;rowstore는 OLTP에 좀 더 적합&lt;/b&gt;하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;colomnstore&lt;/b&gt;는 다음과 같이 열 단위로 저장하는 것을 말한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;Jho&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;Kim&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;Park&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;male&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;male&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;female&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;27&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;29&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;30&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;seoul&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;특정 컬럼(들)에 대한 집계가 필요할 시 필요한 컬럼들이 담긴 디스크 블록들만 읽을 수 있으므로(즉 disk I/O가 감소) 효율적이다. 또한 각 블록들이 가지고 있는 값들은 같은 타입이기 때문에 큰 압축률을 기대할 수 있다는 장점도 있다. 반면 insert, update, delete 수행시 적절한 위치를 찾기 위해 모든 데이터를 순회하므로 비효율적이다. 예를 들어, insert를 할 경우 새로운 레코드의 각 컬럼 값들이 삽입될 적절한 위치를 찾는 과정이 필요하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;즉,&lt;b&gt;&lt;span&gt; column&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;store는 OLAP에 좀 더 적합&lt;/b&gt;하다.&lt;/p&gt;
&lt;p style=&quot;color: #374151; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB</category>
      <category>Columnar</category>
      <category>columnstore</category>
      <category>DB</category>
      <category>Rowstore</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/124</guid>
      <comments>https://jofestudio.tistory.com/124#entry124comment</comments>
      <pubDate>Sun, 4 Feb 2024 19:57:36 +0900</pubDate>
    </item>
    <item>
      <title>PARTITION BY와 GROUP BY의 차이점</title>
      <link>https://jofestudio.tistory.com/123</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Noto Serif KR';&quot;&gt;배경&lt;/span&gt;&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1424&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEbkBs/btsC4EeKRY5/ENZ8aRCLKiJ6KoOku5akTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEbkBs/btsC4EeKRY5/ENZ8aRCLKiJ6KoOku5akTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEbkBs/btsC4EeKRY5/ENZ8aRCLKiJ6KoOku5akTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEbkBs%2FbtsC4EeKRY5%2FENZ8aRCLKiJ6KoOku5akTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;274&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;member는 회원들의 회원번호와 가입일을,&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;sales는 회원별 물품을 주문한 정보를,&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;menu는 각 상품 정보를 가지는 테이블이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 현재 members, sales, menu에 다음과 같이 데이터들이 적재돼있다고 하자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;1. members&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boKSOz/btsC9zpGSnt/JE9MDtUoOkRvNuCgA86o3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boKSOz/btsC9zpGSnt/JE9MDtUoOkRvNuCgA86o3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boKSOz/btsC9zpGSnt/JE9MDtUoOkRvNuCgA86o3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboKSOz%2FbtsC9zpGSnt%2FJE9MDtUoOkRvNuCgA86o3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;149&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;2. sales&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w9RB7/btsC9npibHK/TRPVnemtSW1q3kn1umZ2Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w9RB7/btsC9npibHK/TRPVnemtSW1q3kn1umZ2Yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w9RB7/btsC9npibHK/TRPVnemtSW1q3kn1umZ2Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw9RB7%2FbtsC9npibHK%2FTRPVnemtSW1q3kn1umZ2Yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;457&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;3. menu&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd7Nc9/btsC8U8H00T/kk5XpCsdeOCkTyYyTkpjF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd7Nc9/btsC8U8H00T/kk5XpCsdeOCkTyYyTkpjF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd7Nc9/btsC8U8H00T/kk5XpCsdeOCkTyYyTkpjF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd7Nc9%2FbtsC8U8H00T%2Fkk5XpCsdeOCkTyYyTkpjF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;432&quot; height=&quot;178&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;주어진 문제&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;각 고객들이 구입한 첫 물건은?&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;sales 테이블에는 각 회원들이 특정 물품을 구입한 날에 대한 데이터들이 적재돼있으니, 이 테이블에서 적절한 쿼리를 활용해볼 수 있을 것이다. 이 때 처음 물품을 구매한 날짜에 여러 물품을 구매했다면, 해당 물품들의 정보를 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;모두 출력&lt;/b&gt;&lt;/span&gt;해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;풀이1 : GROUP BY를 활용한 접근&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;sales테이블에 있는 구매한 물품들에 대한 상세정보는 sales와 menu의 product_id들을 INNER JOIN을 걸어 알아낼 수 있다. 그러나 회원별로 가장 처음 구매한 물품들에 대한 정보만 필요하다. sales에서 customer_id로 GROUP BY를 걸고, 그 안에서 MIN함수를 통해 가장 낮은 order_date를 뽑아내면 다음과 같이 회원별로 물품을 처음 구매한 날짜를 추출할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704521682182&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT customer_id, min(order_date)
FROM sales
GROUP BY customer_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;768&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGODgU/btsDau2MDB0/vsSC3MiY2Dd9xEBrzuKJK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGODgU/btsDau2MDB0/vsSC3MiY2Dd9xEBrzuKJK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGODgU/btsDau2MDB0/vsSC3MiY2Dd9xEBrzuKJK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGODgU%2FbtsDau2MDB0%2FvsSC3MiY2Dd9xEBrzuKJK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;346&quot; height=&quot;156&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;위 결과를 WHERE절에서 활용, sales테이블에서 다음과 같이 회원별로 해당 날짜에 대한 데이터들을 추출 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704522137016&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM sales
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;930&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uam1K/btsC1Hwz5g4/0cvNKWjNfwg8j7hf0dDBXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uam1K/btsC1Hwz5g4/0cvNKWjNfwg8j7hf0dDBXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uam1K/btsC1Hwz5g4/0cvNKWjNfwg8j7hf0dDBXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuam1K%2FbtsC1Hwz5g4%2F0cvNKWjNfwg8j7hf0dDBXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;428&quot; height=&quot;206&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 위 결과에서 4, 5번 레코드를 보면 중복되는 레코드들임을 알 수 있다. C는 2021년 1월 1일에 3번 물품을 2번 구매했기 때문에 이런 결과가 생기는 것이다. 따라서 다음과 같이 DISTINCT를 걸어줘서 중복을 제거할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ DISTINCT&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;: &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;SQL에서 중복된 값을 제거하여 결과 집합에서 고유한 값을 반환하는 데 사용되는 키워드. &lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;DISTINCT 뒤에 2개 이상의 컬럼을 사용하면, 해당 컬럼들로 구성되는 조합들에 대한 중복을 제거하는 식으로 동작한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;(즉, 아래 예제에서는 customer_id, order_date, product_id로 이뤄지는 조합에서 중복이 없어진다)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704522294891&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DISTINCT customer_id, order_date, product_id
FROM sales
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;918&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTLyL1/btsC9AoBFdW/qzc51lSpd3gyB6GWJEx0jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTLyL1/btsC9AoBFdW/qzc51lSpd3gyB6GWJEx0jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTLyL1/btsC9AoBFdW/qzc51lSpd3gyB6GWJEx0jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTLyL1%2FbtsC9AoBFdW%2Fqzc51lSpd3gyB6GWJEx0jK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;201&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 menu와 INNER JOIN을 걸어 다음과 같이 처음 구매한 물품의 상세정보를 뽑아낼 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704523072900&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DISTINCT customer_id, order_date, sales.product_id, product_name, price
FROM sales
INNER JOIN menu ON sales.product_id = menu.product_id 
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1422&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4rPAw/btsC4uiXgIb/sSgztrfo3qIyZKfUqgYX9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4rPAw/btsC4uiXgIb/sSgztrfo3qIyZKfUqgYX9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4rPAw/btsC4uiXgIb/sSgztrfo3qIyZKfUqgYX9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4rPAw%2FbtsC4uiXgIb%2FsSgztrfo3qIyZKfUqgYX9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;176&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리를 좀만 다듬으면(사실 SELECT절에서 필요한 정보만 뽑도록 하면), 다음과 같이 회원 아이디와 상품명만 추출 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704523292012&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DISTINCT customer_id, product_name
FROM sales
INNER JOIN menu ON sales.product_id = menu.product_id 
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;682&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc2288/btsDbcufgsw/pXvSJTxjEKNkLXSK31QTtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc2288/btsDbcufgsw/pXvSJTxjEKNkLXSK31QTtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc2288/btsDbcufgsw/pXvSJTxjEKNkLXSK31QTtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc2288%2FbtsDbcufgsw%2FpXvSJTxjEKNkLXSK31QTtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;307&quot; height=&quot;180&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;풀이 2 : PARTITION BY를 이용한 접근&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 이 문제는 PARTITION BY를 이용해서도 풀 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ PARTITION BY?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;: 특정 그룹 내에서의 집계 또는 순위를 매길 때 사용하는 함수.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(집계함수 : MIN, MAX, SUM 등을 말함)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(순위함수 : ROW_NUMBER, RANK 등을 말함)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  GROUP BY와의 차이점?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;: GROUP BY, PARTITION BY 모두 특정 컬럼을 기준으로 레코드들을 그룹핑한다는 공통점이 있다. 그러나 GROUP BY는 그룹핑되는 과정에서 레코드들의 상세 정보들이 없어지는 반면(레코드들이 집약되기 때문), PARTITION BY는 그룹핑돼도 레코드들의 상세 정보가 남아있게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;sales테이블에 대해 회원별로 구매 횟수를 알고 싶다고 할 때, GROUP BY와 PARTITION BY에 대한 다음 결과를 비교해보면 더욱 잘 이해할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;1) GROUP BY로 그룹핑해서 계산하는 경우&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704525039555&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT customer_id, count(*) as purchased_count
FROM sales
GROUP BY customer_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;758&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6rbzF/btsC6TiaIJh/qkSO4pCg1ALYqHzgKXH0O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6rbzF/btsC6TiaIJh/qkSO4pCg1ALYqHzgKXH0O0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6rbzF/btsC6TiaIJh/qkSO4pCg1ALYqHzgKXH0O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6rbzF%2FbtsC6TiaIJh%2FqkSO4pCg1ALYqHzgKXH0O0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;345&quot; height=&quot;164&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;sales 테이블이 갖고 있던 레코드들이 나오는게 아니라, 집계 결과만 나오는 모습을 확인 가능하다. 만약 order_date나 product_id를 SELECT에서 찍어서 보려고 하면, 1055 ERROR가 난다 (GROUP BY가 사용된 경우, GROUP BY로 그룹핑할 때 사용된 컬럼만 SELECT에서 명시가 가능하기 때문)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;2) PARTITION BY로 그룹핑해서 계산하는 경우&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704525508008&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT customer_id, count(*) OVER (PARTITION BY customer_id) as purchased_count
FROM sales;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;738&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGvKX/btsDayREBXO/GdgDKc1xXawGM5n2Ivn85k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGvKX/btsDayREBXO/GdgDKc1xXawGM5n2Ivn85k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGvKX/btsDayREBXO/GdgDKc1xXawGM5n2Ivn85k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGvKX%2FbtsDayREBXO%2FGdgDKc1xXawGM5n2Ivn85k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;354&quot; height=&quot;458&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;sales테이블이 갖고 있는 레코드들이 살아있음을 확인할 수 있다. SELECT절에서도 order_date, product_id컬럼값을 뽑아낼 수 있으며 에러가 나지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704525638136&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT customer_id, order_date, product_id, count(*) OVER (PARTITION BY customer_id) as purchased_count
FROM sales;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1314&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kX8pI/btsC85vDvlM/gKlYOcAjgx6pAeUoLYl0PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kX8pI/btsC85vDvlM/gKlYOcAjgx6pAeUoLYl0PK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kX8pI/btsC85vDvlM/gKlYOcAjgx6pAeUoLYl0PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkX8pI%2FbtsC85vDvlM%2FgKlYOcAjgx6pAeUoLYl0PK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;587&quot; height=&quot;430&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;962&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;RANK순위 함수와 ORDER BY를 사용하면 그룹핑된 섹션 내에서 order_date를 기준으로 다음과 같이 순위를 매길 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704526673968&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    customer_id,
    order_date,
    product_name,
    RANK() OVER (PARTITION BY customer_id ORDER BY order_date) as order_date_rank
FROM sales
INNER JOIN menu ON sales.product_id = menu.product_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1368&quot; data-origin-height=&quot;948&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvHXO6/btsC51U8AdU/Xk05POINzs3hAm2ooIUROK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvHXO6/btsC51U8AdU/Xk05POINzs3hAm2ooIUROK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvHXO6/btsC51U8AdU/Xk05POINzs3hAm2ooIUROK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvHXO6%2FbtsC51U8AdU%2FXk05POINzs3hAm2ooIUROK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;585&quot; height=&quot;405&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그룹핑된 각 섹션 내에서 order_date값이 같으면 같은 rank가 매겨진다. RANK와는 달리 DENSE_RANK를 사용하면 동일한 등수가 발생해도 다음번 등수는 이전 등수 + 1이 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;위 결과를 WHERE절에서 활용, order_date_rank가 1인 레코드만 추출하면 원하는 결과를 가져올 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704541940484&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH ranked_date as (
    SELECT
        customer_id,
        order_date,
        product_name,
        RANK() OVER (PARTITION BY customer_id ORDER BY order_date) as order_date_rank
    FROM sales
    INNER JOIN menu ON sales.product_id = menu.product_id
)

SELECT DISTINCT customer_id, product_name
FROM ranked_date
WHERE order_date_rank = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;688&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TvWBa/btsC2BiFxJm/Sc7OPYswQYHhiOREjfvkSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TvWBa/btsC2BiFxJm/Sc7OPYswQYHhiOREjfvkSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TvWBa/btsC2BiFxJm/Sc7OPYswQYHhiOREjfvkSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTvWBa%2FbtsC2BiFxJm%2FSc7OPYswQYHhiOREjfvkSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;342&quot; height=&quot;199&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Noto Serif KR';&quot;&gt;더 적합한 방식은?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 소개한 GROUP BY와 PARTITION BY 모두 같은 결과를 뱉는다. 그러면 둘 중 어느 것이 좀 더 &quot;적합&quot;할까? 어느 것이 더 효율적으로 동작하는가? 지금은 sales테이블에 데이터가 끽해야 15개 남짓이지만, x억개 있는 상황이라면 뭐가 더 나을까?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리 실행 계획을 살펴보며 어떤 것이 더 나을지 살펴보자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;문제 출처 : 8 week SQL Challenge&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://8weeksqlchallenge.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://8weeksqlchallenge.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704543242208&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;8 Week SQL Challenge&quot; data-og-description=&quot;Start your SQL learning journey today!&quot; data-og-host=&quot;8weeksqlchallenge.com&quot; data-og-source-url=&quot;https://8weeksqlchallenge.com/&quot; data-og-url=&quot;http://www.8weeksqlchallenge.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/r03wd/hyUXOjLvfB/7aNa57VcUhSisXuNfXCKo1/img.png?width=1280&amp;amp;height=720&amp;amp;face=176_148_1066_370,https://scrap.kakaocdn.net/dn/s2k0T/hyU2nY6JWZ/YNYuhL51BAW5tsmTk3ikK0/img.png?width=1280&amp;amp;height=720&amp;amp;face=176_148_1066_370,https://scrap.kakaocdn.net/dn/8WJ4T/hyUXXHNJjt/s4zNgbNUHTlzcu3orT8KKk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080&quot;&gt;&lt;a href=&quot;https://8weeksqlchallenge.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://8weeksqlchallenge.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/r03wd/hyUXOjLvfB/7aNa57VcUhSisXuNfXCKo1/img.png?width=1280&amp;amp;height=720&amp;amp;face=176_148_1066_370,https://scrap.kakaocdn.net/dn/s2k0T/hyU2nY6JWZ/YNYuhL51BAW5tsmTk3ikK0/img.png?width=1280&amp;amp;height=720&amp;amp;face=176_148_1066_370,https://scrap.kakaocdn.net/dn/8WJ4T/hyUXXHNJjt/s4zNgbNUHTlzcu3orT8KKk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;8 Week SQL Challenge&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Start your SQL learning journey today!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;8weeksqlchallenge.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>DB/MySQL</category>
      <category>group</category>
      <category>partition</category>
      <category>sql</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/123</guid>
      <comments>https://jofestudio.tistory.com/123#entry123comment</comments>
      <pubDate>Sat, 6 Jan 2024 21:14:46 +0900</pubDate>
    </item>
    <item>
      <title>mysqldump를 활용한 DB 백업</title>
      <link>https://jofestudio.tistory.com/122</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;mysqldump?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말해 mysql db의 상태를 덤프를 떠서 그대로 백업뜨는 걸 말한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;sql파일이 만들어지며, dump를 뜬 시점에서 db가 가지고 있던 테이블들과 내용들에 대한 create, insert문들이 들어가있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이를 활용해 로컬에서 다루던 db를 그래도 배포 환경에 옮기는 작업, 배포 환경에 있던 db를 로컬로 옮기는 작업 등을 수행할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;본 포스트에선 AWS RDS에 있는 mysql 데이터를 덤프떠서 로컬로 가져와본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;AWS RDS에 넣어뒀던 data들을 dump를 떠서 로컬로 가져오려고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;DB 관리를 위해 띄워뒀던 Bastion에 접속해 다음과 같은 커맨드를 날리면 된다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700996404223&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql -h {RDS 엔드포인트} -u {유저명} -p {dump할 DB명} &amp;gt; {경로와 생성할 sql파일의 이름}.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이때 permission denied 에러가 날 수 있는데, 이는 생성할 sql파일의 경로를 ~와 같이 자신에게 권한이 부여된 경로를 작성하면 된다. 내 경우 다음과 같이 커맨드를 날렸다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700996095795&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysqldump -h somsatangdb.c7jz8i0idkiy.ap-northeast-2.rds.amazonaws.com -u swtAdmin -p wooyoungsoo &amp;gt; ~/petdori.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 다음 로컬에서 scp 커맨드를 통해 EC2에 위치한 dump파일을 로컬로 가져온다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700997553827&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;scp -i {pem파일 경로} &quot;{EC2 사용자명}@{EC2 ip}:{다운할 dump파일의 경로와 이름}&quot; {다운받을 파일의 로컬에서의 경로 및 이름}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;별도 설정을 하지 않았다면, EC2 사용자명은 ubuntu는 ubuntu, amazon linux는 ec2-user다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 로컬에서 커맨드를 날리는 것이므로 EC2에 적절한 보안그룹을 설정해줘야 한다는 사실을 잊으면 안 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;내 경우 다음과 같이 커맨드를 날렸다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700997481708&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;scp -i ./sst.pem &quot;ubuntu@13.124.138.97:~/petdori.sql&quot; ./petdori.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같이 dump파일이 잘 다운받아진 걸 확인 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;330&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X0KdO/btsAXe87gpf/R0OgxA1KS0gpHo3FrKqQw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X0KdO/btsAXe87gpf/R0OgxA1KS0gpHo3FrKqQw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X0KdO/btsAXe87gpf/R0OgxA1KS0gpHo3FrKqQw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX0KdO%2FbtsAXe87gpf%2FR0OgxA1KS0gpHo3FrKqQw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;330&quot; height=&quot;134&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같이 rds에서 가지고 있던 테이블과 내용물들에 대한 쿼리가 작성돼있는 걸 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2296&quot; data-origin-height=&quot;1576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eYrRxk/btsAUoK9uwC/5B9v5QzcimNvtA3y2EbVE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eYrRxk/btsAUoK9uwC/5B9v5QzcimNvtA3y2EbVE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eYrRxk/btsAUoK9uwC/5B9v5QzcimNvtA3y2EbVE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeYrRxk%2FbtsAUoK9uwC%2F5B9v5QzcimNvtA3y2EbVE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;701&quot; height=&quot;481&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;1576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 다음, 나는 로컬에서 도커 컨테이너로 mysql을 사용했던만큼, 컨테이너 내부로 dump파일을 옮겨야 했다. 다음 형태로 커맨드를 입력하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700999202625&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker cp {로컬에서의 dump경로 및 이름} {컨테이너명}:{dump파일을 둘 컨테이너 내 경로}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;내 경우 다음과 같이 입력했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700999151328&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker cp ./Downloads/petdori.sql mysqlDB:/&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 docker exec을 가미해 다음과 같은 형태의 커맨드를 날리면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700999585251&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql -u {사용자명} -p {DB명} &amp;lt; {덤프파일}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 지정한 DB에 덤프파일에 있던 쿼리들이 수행된다. 내 경우 다음과 같이 커맨드를 날렸다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700999600374&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql -u root -p wooyoungsoo &amp;lt; petdori.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같이 RDS상에 있던 테이블과 내용물들이 잘 들어가있는 걸 볼 수 있었다!&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;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCz3hP/btsATBc1FL7/enn63LXxi6P2KA9RffvAtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCz3hP/btsATBc1FL7/enn63LXxi6P2KA9RffvAtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCz3hP/btsATBc1FL7/enn63LXxi6P2KA9RffvAtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCz3hP%2FbtsATBc1FL7%2Fenn63LXxi6P2KA9RffvAtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;481&quot; height=&quot;449&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2410&quot; data-origin-height=&quot;948&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bllKjB/btsASeJN4XX/xD1Xp4jMJVBTmKyvEDiF1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bllKjB/btsASeJN4XX/xD1Xp4jMJVBTmKyvEDiF1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bllKjB/btsASeJN4XX/xD1Xp4jMJVBTmKyvEDiF1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbllKjB%2FbtsASeJN4XX%2FxD1Xp4jMJVBTmKyvEDiF1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2410&quot; height=&quot;948&quot; data-origin-width=&quot;2410&quot; data-origin-height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MySQL</category>
      <category>MySQL</category>
      <category>mysqldump</category>
      <category>백업</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/122</guid>
      <comments>https://jofestudio.tistory.com/122#entry122comment</comments>
      <pubDate>Sun, 26 Nov 2023 20:53:57 +0900</pubDate>
    </item>
    <item>
      <title>리눅스에서의 사용자, 그룹 관리</title>
      <link>https://jofestudio.tistory.com/121</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;/etc/passwd&amp;nbsp; 파일&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사용자들의 계정 정보가 저장된 파일이다. 다음과 같은 구조이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-24 오전 1.11.20.png&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmNaC/btsyUH8ha4W/oSvrKq8TZnndEbP2j8z5Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmNaC/btsyUH8ha4W/oSvrKq8TZnndEbP2j8z5Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmNaC/btsyUH8ha4W/oSvrKq8TZnndEbP2j8z5Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlmNaC%2FbtsyUH8ha4W%2FoSvrKq8TZnndEbP2j8z5Gk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;763&quot; height=&quot;47&quot; data-filename=&quot;스크린샷 2023-10-24 오전 1.11.20.png&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;유저명:x:UID:GID:설명:홈 디렉토리:로그인 쉘&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;유저명 : 말 그대로 유저 이름&lt;/li&gt;
&lt;li&gt;x : 원래 초기 유닉스 시스템에선 사용자 암호가 이쪽에 저장됐으나, 현재 사용자 암호는 /etc/shadow에 있다(해쉬된 형태로)&lt;/li&gt;
&lt;li&gt;UID : 사용자들에게 할당되는 고유한 ID 번호&lt;/li&gt;
&lt;li&gt;GID : 그룹 ID로, 리눅스에서 사용자는 무조권 한 개 이상의 그룹에 소속돼있다.&lt;/li&gt;
&lt;li&gt;설명 : 사용자의 실명, 부서명 같은 것들&lt;/li&gt;
&lt;li&gt;홈 디렉토리 : 해당 사용자 계정에 할당된 홈 디렉토리 경로 (ex: /home/jofe 등)&lt;/li&gt;
&lt;li&gt;로그인 쉘 : 사용자의 로그인 쉘을 지정(로그인 후 켜지는 쉘)&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;/etc/shadow 파일&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사용자 암호에 대한 정보를 별도로 관리하는 파일이다. 전지전능한 root께서만 읽고 쓸 수 있으며 shadow그룹은 읽기만 가능하다. 다음과 같은 구조다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2232&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oVGjj/btsyVhaPozv/6PVllpmOhx22brWliTuxCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oVGjj/btsyVhaPozv/6PVllpmOhx22brWliTuxCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oVGjj/btsyVhaPozv/6PVllpmOhx22brWliTuxCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoVGjj%2FbtsyVhaPozv%2F6PVllpmOhx22brWliTuxCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2232&quot; height=&quot;72&quot; data-origin-width=&quot;2232&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;유저명:암호:최종변경일:MIN:MAX:WARNING:INACTIVE:EXPIRE:Flag&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;유저명 : 말 그대로 유저 이름&lt;/li&gt;
&lt;li&gt;암호 : 해쉬된 형태의 암호가 있다&lt;/li&gt;
&lt;li&gt;최종 변경일 : 1970년 1월 1일을 기준으로 최종 변경된 날을 표기&lt;/li&gt;
&lt;li&gt;MIN : 비밀번호 변경 주기를 의미(암호 변경 후 사용해야 하는 최소 기간)&lt;/li&gt;
&lt;li&gt;MAX : 비밀번호를 사용가능한 최대 기간. 즉 유효 기간이라 생각하면 됨&lt;/li&gt;
&lt;li&gt;WARNING : 암호가 만료되기 x일 전부터 알려주겠다 할 때 x에 해당&lt;/li&gt;
&lt;li&gt;INACTICE : 암호가 만료된 후에도 이 값으로 지정된 날수 동안은 로그인 가능(마지막으로 삶을 돌아보는 시간..)&lt;/li&gt;
&lt;li&gt;EXPIRE : 사용자 계정이 만려되는 날짜(1970년 1월 1일 기준)&lt;/li&gt;
&lt;li&gt;Flag : 향후 사용할 목적으로 비워둔 항목&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 암호의 경우 단순히 해쉬된 암호만 띡하니 있지 않고, 사용된 해쉬 알고리즘의 타입(MD5, SHA)와 salt값도 동봉돼있다. 비번을 해쉬할 때 단순히 비밀번호만 해쉬하는게 아니라 랜덤하게 만들어낸 salt값을 함께 해싱하는데, 이를 통해 유저 A, B가 똑같은 평문 비밀번호를 사용해도 salt값이 달라서 전체적인 해쉬 값은 다른 걸 가지게 된다. 보안에도 용이한 셈.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;/etc/group 파일&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;각 그룹들의 정보가 저장된 파일. 참고로 각 사용자마다 기본 그룹이라는게 있는데, /etc/passwd에 사용자별로 가지는 gid가 바로 그것이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-24 오전 1.09.50.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsgFGI/btsyWgbD5Z4/tsDIjkiEyEO0Te9sfT8Xdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsgFGI/btsyWgbD5Z4/tsDIjkiEyEO0Te9sfT8Xdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsgFGI/btsyWgbD5Z4/tsDIjkiEyEO0Te9sfT8Xdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsgFGI%2FbtsyWgbD5Z4%2FtsDIjkiEyEO0Te9sfT8Xdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;507&quot; height=&quot;69&quot; data-filename=&quot;스크린샷 2023-10-24 오전 1.09.50.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;그룹명:x:GID:그룹 멤버&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;그룹명 : 말 그대로 그룹 이름&lt;/li&gt;
&lt;li&gt;x : 그룹의 암호 저장(그룹 암호는 newgrp 커맨드로 자신이 속하지 않은 그룹으로 전환할 때 필요함)&lt;/li&gt;
&lt;li&gt;GID : 그룹을 식별하는 고유 번호&lt;/li&gt;
&lt;li&gt;그룹 멤버 : 해당 그룹에 속하는 멤버들의 을 쉼표로 구분해서 이 영역에 표기&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;/etc/gshadow 파일&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그룹 암호가 저장된 파일&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;그룹명:그룹 암호:관리자:그룹 멤버&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;그룹명 : 말 그대로 그룹 이름&lt;/li&gt;
&lt;li&gt;그룹 암호 : 암호화된 그룹 암호&lt;/li&gt;
&lt;li&gt;관리자 : 그룹의 암호나 멤버를 바꿀 수 있는 사용자 계정. 여러 개면 쉼표로 구분&lt;/li&gt;
&lt;li&gt;그룹 멤버 : 그룹에 속한 멤버들의 계정명. 쉼표로 구분함&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리눅스에서 사용자 계정 추가하기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. useradd 커맨드 활용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으론 &quot;useradd 유저명&quot;을 통해 사용자 계정 생성 가능&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;-u, -g 등의 옵션을 붙여 UID나 GID 등을 직접 지정하는 것이 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;정말 아무것도 없이 계정만 생성해주므로, 비밀번호 등은 별도로 설정해줘야 한다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. adduser 커맨드 활용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으론 &quot;adduser 유저명&quot;을 통해 사용자 계정 생성 가능&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;-uid, -gid 등의 옵션을 붙여 UID, GID 등을 직접 지정하는 것이 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;생성 시 비밀번호 등도 입력받게끔 만들어져있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;비슷하게, groupadd커맨드를 통해 기존에 없던 그룹 생성도 가능하며, -g 등의 옵션을 곁들여 GID지정도 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리눅스에서 사용자 계정 수정하기 - usermod 커맨드 활용&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;usermod [옵션] [유저명]&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;-u, -g 등의 옵션으로 UID, 기본 그룹 등의 수정이 가능하다. -aG옵션을 통해 해당 유저를 sudo등의 그룹에 넣을 수도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;비슷하게, groupmod커맨드를 통해 그룹의 GID등을 수정가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리눅스에서 사용자 계정 삭제 - userdel 커맨드 활용&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;userdel [옵션] [유저명]&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;-r 옵션을 줘서 홈 디렉토리도 삭제할 수 있고, -f옵션을 통해 해당 사용자가 로그인 중이어도 강제 삭제가 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;비슷하게, groupdel커맨드를 통해 그룹을 삭제할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리눅스에서 소속 그룹 변경하기 - newgrp 커맨드 활용&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;newgrp [그룹명]&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;말 그대로 사용자가 작업시 현재 소속 그룹을 기본 그룹이 아닌 다른 그룹으로 바꿀 때 사용하는 커맨드다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;UID vs EUID&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;UID : 실제 유저의 ID다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;EUID : 유효한 사용자 ID로, 현재 명령을 누구 권한으로 수행하고 있는가에 해당하는 개념이다. 즉, UID가 a인 상태에서 커맨드를 실행했을 때 EUID는 a가 아니라 b가 될 수도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;파일/디렉토리의 소유자 변경하기&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;chown [옵션] [사용자계정] [파일 또는 디렉토리명]&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ex) chown root file1 : file1의 소유자를 root로 바꾼다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Linux</category>
      <category>Linux</category>
      <category>그룹</category>
      <category>리눅스</category>
      <category>사용자</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/121</guid>
      <comments>https://jofestudio.tistory.com/121#entry121comment</comments>
      <pubDate>Thu, 26 Oct 2023 20:32:41 +0900</pubDate>
    </item>
    <item>
      <title>Redis의 개념과 Redis에서 고가용성(High Availability)를 확보하는 방법</title>
      <link>https://jofestudio.tistory.com/120</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;레디스(Redis)&lt;/b&gt;&lt;b&gt;의 개념&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Remote Dictionary Server의 줄임말로, &lt;b&gt;key&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;&lt;b&gt;-value 구조의 비정형 데이터&lt;/b&gt;를 저장하고 관리하기 위한 오픈 소스 기반의 &lt;b&gt;NoSQL DB&lt;/b&gt;다. 타 DB처럼 디스크에 데이터를 저장하는게 아닌 &lt;b&gt;메모리에 데이터를 저장하는 인-메모리 DB&lt;/b&gt;이며, 메모리를 통한 빠른 속도 때문에 Cache 용도로 많이 활용되지만 그 외에도 Message Broker나 Streaming engine으로서의 역할도 맡을 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;Message broker로 쓰인다는 것의 의미 : &lt;span style=&quot;text-align: start;&quot;&gt;분산 시스템에서 메시지 전달 및 이벤트 처리를 관리하는 소프트웨어 컴포넌트로 활용된다는 것&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Streaming engine로 쓰인다는 것의 의미 : 실시간 데이터 처리를 위한 목적으로 활용된다는 것&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;레디스에서 고가용성을 확보하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. Stand alone (No HA)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6h0vB/btsyLNAWKj1/YlCinFZmh7KKCKc5eA9akk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6h0vB/btsyLNAWKj1/YlCinFZmh7KKCKc5eA9akk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6h0vB/btsyLNAWKj1/YlCinFZmh7KKCKc5eA9akk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6h0vB%2FbtsyLNAWKj1%2FYlCinFZmh7KKCKc5eA9akk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;212&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Redis 서버 1대로 아키텍처를 구성하는 방법을 말하며, 해당 서버가 다운되면 &quot;레디스 사용 불가&quot;이므로 고가용성이 보장되지 않는다. 서버가 다운될 시 AOF 또는 snapshot을 사용해 재시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AOF : Append Only File의 줄임말로, &lt;span style=&quot;text-align: left;&quot;&gt;Redis의 변경 사항을 기록하는 파일을 말한다. &lt;span style=&quot;text-align: left;&quot;&gt;Redis 서버에 새로운 명령(조회는 제외)이나 데이터 변경이 발생하면, 해당 명령 또는 변경 내용을 AOF 파일에 연속적으로 추가한다. 이는 변경 사항을 로그로 남기는 방식이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; snapshot : &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;특정 시점에 메인 메모리에 있는 모든 레디스 데이터를 디스크에&amp;nbsp; 것, 즉 일종의 백업을 말한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;2. 이중화 (a.k.a Master &amp;amp; Slave, Half HA)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&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;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nX4k7/btsyQQClOkT/AMYiokmhra0NRQw6zSrMK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nX4k7/btsyQQClOkT/AMYiokmhra0NRQw6zSrMK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nX4k7/btsyQQClOkT/AMYiokmhra0NRQw6zSrMK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnX4k7%2FbtsyQQClOkT%2FAMYiokmhra0NRQw6zSrMK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;232&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;Redis 서버 2대로 아키텍처를 구성하는 것을 말하며, Slave는 Master의 데이터를 실시간으로 전달받아 보관한다. Master가 다운될 시 Slave를 FailOver시킬 수 있으나 &lt;b&gt;수동&lt;/b&gt;으로 직접 해줘야 한다. Slave를 하나가 아닌 여러 개를 둘 수도 있다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;3. 이중화 + 센티널 (HA, 무중단 서비스 가능)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-10-19 오후 10.52.44.png&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn3Fey/btsyMbnXrV4/6KfBjGI5opvl8d5k1KE9Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn3Fey/btsyMbnXrV4/6KfBjGI5opvl8d5k1KE9Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn3Fey/btsyMbnXrV4/6KfBjGI5opvl8d5k1KE9Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn3Fey%2FbtsyMbnXrV4%2F6KfBjGI5opvl8d5k1KE9Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1526&quot; height=&quot;1140&quot; data-filename=&quot;스크린샷 2023-10-19 오후 10.52.44.png&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Master &amp;amp; Slave 구성에 센티널(Sentinal)을 추가해서 각 서버를 감시하도록 하도록 하는 아키텍처를 구성하는 것을 말한다. 센티널은 Master를 감시하고 있다가 Master가 다운되면 Slave를 Master로 승격시킨다. Redis Client(즉 Application)은 새로운 Master로 접속해서 서비스를 계속한다. 센티널은 데이터 처리는 담당하지 않으며, 센티널 자체가 다운되는 상황을 고려해 일반적으로 3대의 센티널을 운용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;센티널이란 : &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;Master와 Slave들을 감시하고 있다가 Master가 다운되면 이를 감지해서 관리자의 개입없이 자동으로 Slave를 Master로 올려주는 감시자(보초)를 말한다. 즉 센티널은 감시, 자동 장애조치(Automatic FailOver)의 역할을 하며 알림(FailOver될 때 관리자한테 메일 보내던가 하는..)의 역할도 맡을 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;4. 레디스 클러스터 (HA)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1984&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGC5iP/btsyMMuIx5D/XNbY5gElyhLcrdEKAYuogK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGC5iP/btsyMMuIx5D/XNbY5gElyhLcrdEKAYuogK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGC5iP/btsyMMuIx5D/XNbY5gElyhLcrdEKAYuogK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGC5iP%2FbtsyMMuIx5D%2FXNbY5gElyhLcrdEKAYuogK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1984&quot; height=&quot;580&quot; data-origin-width=&quot;1984&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;샤딩(sharding)을 사용하여 복수의 Redis노드에 데이터를 분할하는 방식으로 아키텍처를 구성하는 것을 말한다. Master가 3대라면, 전체 데이터를 3대에 나누어 저장하는 것이다. (100개가 있다면 1번에 33, 2번에 33, 3번에 34개 이런 식으로). 데이터들의 key에 hash함수를 멕인 값에 따라 어느 Master 서버로 데이터를 둘지 결정하게 된다. 각 Master 서버가 데이터 처리 뿐만 아니라 센티널 역할도 같이 수행하며, &lt;b&gt;최소 3대의 Master 서버가 필요&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;샤딩을 통해 데이터들을 분할해서 각 Master 서버에 저장하기 때문에 하나의 서버라도 다운되면 데이터 유실이 생기지 않을까라는 생각을 할 수 있으나, 위 그럼처럼 클러스터를 구성하면 &lt;b&gt;아무리 Master들이 다운되도 하나의 서버만 살아있다면 정상적인 운용이 가능&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클러스터 방식을 통해 여러 대의 서버가 하나로 묶여 마치 1개의 시스템처럼 동작하게 되며, &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;여러 서버에 데이터를 분산하여 저장하기 때문에 부하를 여러 대의 서버로 분산시키므로 더 빠른 속도로 사용자에게 서비스를 제공할 수 있게 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;샤딩(Sharding)이란 :&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;대량의 데이터를 처리하기 위해 여러 개의 데이터베이스에 분할하는 기술 즉 DBMS안에서 데이터를 나누는 것이 아니라 DBMS 밖에서 데이터를 나누는 방식임에 유의하자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;클러스터 vs 센티널&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;우선 둘 다 고가용성을 챙길 수 있다. 그러나 클러스터는 확장성이 있는 반면, 센티널은 확장성이 없는 아키텍처다. 또한 클러스터는 센티널에 비해 빠른 액세스 속도를 보일 수 있다. 반면 센티널은 클러스터에 비해 배포 및 관리가 용이하다. 따라서, 일반적으로 중소 정도의 규모라면 센티널을, 수평적 확장이 필요한 대규모의 경우는 클러스터를 추천한다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고한 자료&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;http://redisgate.kr/redis/configuration/redis_overview.php&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;http://redisgate.kr/redis/configuration/redis_overview.php&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1697720631883&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;Redis Architecture Overview&quot; data-og-description=&quot;&quot; data-og-host=&quot;redisgate.kr&quot; data-og-source-url=&quot;http://redisgate.kr/redis/configuration/redis_overview.php&quot; data-og-url=&quot;http://redisgate.kr/redis/configuration/redis_overview.php&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qb5Cn/hyUgFT5vb9/wmzW8ctlkUUeHUuoFaPVa0/img.png?width=531&amp;amp;height=266&amp;amp;face=0_0_531_266,https://scrap.kakaocdn.net/dn/cXW64r/hyUgMsaqCX/SPBzHoJ1PWyiVnUhlZzjH0/img.png?width=459&amp;amp;height=262&amp;amp;face=0_0_459_262,https://scrap.kakaocdn.net/dn/eqv6tn/hyUgPoSx88/sRm8eg4Qye8Zg13cc4yMZK/img.png?width=460&amp;amp;height=260&amp;amp;face=0_0_460_260&quot;&gt;&lt;a href=&quot;http://redisgate.kr/redis/configuration/redis_overview.php&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://redisgate.kr/redis/configuration/redis_overview.php&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qb5Cn/hyUgFT5vb9/wmzW8ctlkUUeHUuoFaPVa0/img.png?width=531&amp;amp;height=266&amp;amp;face=0_0_531_266,https://scrap.kakaocdn.net/dn/cXW64r/hyUgMsaqCX/SPBzHoJ1PWyiVnUhlZzjH0/img.png?width=459&amp;amp;height=262&amp;amp;face=0_0_459_262,https://scrap.kakaocdn.net/dn/eqv6tn/hyUgPoSx88/sRm8eg4Qye8Zg13cc4yMZK/img.png?width=460&amp;amp;height=260&amp;amp;face=0_0_460_260');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Redis Architecture Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redisgate.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/Redis</category>
      <category>Redis</category>
      <category>고가용성</category>
      <category>레디스</category>
      <category>센티널</category>
      <category>클러스터</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/120</guid>
      <comments>https://jofestudio.tistory.com/120#entry120comment</comments>
      <pubDate>Thu, 19 Oct 2023 23:34:51 +0900</pubDate>
    </item>
    <item>
      <title>HTTPS에 대한 짧은 지식</title>
      <link>https://jofestudio.tistory.com/119</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;저작자의 허락을 받고 아래 영상의 내용을 요약한 글입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=H6lpFRpyl14&amp;amp;t=268s&quot;&gt;https://www.youtube.com/watch?v=H6lpFRpyl14&amp;amp;t=268s&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=H6lpFRpyl14&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/DMzgJ/hyTVRHmoBz/UND2N1Z8rQNQfd4NNOiw4k/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/H6lpFRpyl14&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;HTTPS : Http + Secure. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;기존의 HTTP에 &quot;안전&quot;을 더한 형태&lt;/b&gt;&lt;/span&gt;. 그럼 뭘 안전하게 하는 것인가? 크게 2가지로 볼 수 있다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내가 사이트에 보내는 정보들을 제 3자가 못 보게 한다&lt;/li&gt;
&lt;li&gt;내가 접속하는 사이트가 신뢰할 수 있는 사이트인지 알려준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 이 두 가지 내용이 어떤 원리로 구현되는가?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;1은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;비대칭키를 사용한 암호화로 가능&lt;/b&gt;&lt;/span&gt;하다. 서버가 가진 &lt;b&gt;&quot;공개키&quot;&lt;/b&gt;로 암호화해서 보내면, 해당 암호문은 &lt;b&gt;&quot;개인키&quot;&lt;/b&gt;를 가진 서버만이 해독 가능하다. 따라서 제 3자가 내가 보내는 트래픽을 엿봐도 해독(복호화)할 수 없다. (제 3자도 서버의 공개키를 알 수는 있겠지만, 비대칭키의 특성상 공개키로 암호화한 내용을 공개키로 복호화할 수 없기 때문)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;2도 마찬가지로 비대칭키 특성을 통해 가능하다. 서버에서 보내는 정보는 서버의 &lt;b&gt;&quot;개인키&quot;&lt;/b&gt;로 일부가 암호화돼있다. 그러나 내가 가진 서버의 &lt;b&gt;&quot;공개키&quot;&lt;/b&gt;로는 오직 해당 서버의 &lt;b&gt;&quot;개인키&quot;&lt;/b&gt;로 암호화한 것만 복호화가 가능하다. 즉 해당 서버가 아닌 다른 서버에서 온 내용은 내가 가진 공개키로는 복호화가 안 된다는 거다. 이 때 신뢰가능한, 즉 공인된 특정 기관에서 내가 가진 공개키가 해당 서버의 공개키가 맞다는 인증만 받는다면 해당 서버를 신뢰할 수 있게 되는 것.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 프로그래머의 영역에서, HTTPS는 실제로 어떻게 구현될까?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우선 jofe라는 사이트가 공개키들을 뿌렸다고 가정하자. 난 이 공개키가 jofe란 사이트의 공개키인지(즉 정품인지)를 확인할 수 있어야 한다. 이를 인증해주는(보장해주는) 공인된 기관들이 존재하며, 이들을 CA(Certificate Authority)라고 부른다. 우리가 사용중인 브라우저들엔 이 CA들의 목록이 저장돼있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황에서, 내(client)가 jofe(server)에 접속할려고 한다. 랜덤한 데이터나 만들어서 서버에게 주면, 서버는 그에 대한 답변으로 랜덤한 데이터와 &quot;CA에서 발급해준 해당 서버의 인증서&quot;를 만들어서 내게 준다. (클라이언트 서버가 악수했다고 표현)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 클라이언트는 해당 인증서가 진품인지를 판단하는데, 이 때 브라우저에 내장된 CA들의 정보를 통해 확인한다. CA의 인증을 받은 인증서들은 해당 CA의 개인키로 암호화돼있기 때문에 그 CA의 공개키로 복호화가 가능하다. 즉 받은 인증서가 진품이라면 브라우저에 있는 C의 공개키로 해당 인증서를 복호화 가능한 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;성공적으로 복호화된 인증서에는 인증서를 보낸 서버(jofe)의 &lt;b&gt;&quot;공개키&quot;&lt;/b&gt;가 들어있다. 하지만 그렇다고 이제부터 바로 내가 서버로 보낼 메시지들을 공개키로 암호화해서 보낼 수 있는 건 아니다. 왜냐하면 비대칭키는 대칭키 방식보다 컴퓨터에 주는 부담이 크기 때문. (주고받을 다량의 데이터를 하나하나 비대칭키로 암호화하는 건 부담이 된다는 것)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 주고받을 데이터를 &lt;b&gt;&quot;대칭키&quot;&lt;/b&gt;로 암호화한다. 대칭키를 사용하는 경우의 가장 큰 단점은 양쪽이 똑같은 키를 가져야 하므로 어쨌거나 한 번은 서로 통신을 해서 키를 주고받아야 하는데, 이 때 탈취당할 위험이 있다는 거다. 이 때 클라이언트는 아까 악수할 때 만들어진 데이터들을 이용해 임의의 대칭키를 만든 다음, 서버로 이를 공유할 때 &lt;b&gt;&quot;비대칭키&quot;&lt;/b&gt;를 사용한다. 즉 서버의 &lt;b&gt;&quot;공개키&quot;&lt;/b&gt;로 임의로 만든 대칭키를 암호화하여 보낸다는 말. 서버는 이를 자신의 &lt;b&gt;&quot;개인키&quot;&lt;/b&gt;로 복호화하여 클라이언트가 만든 대칭키를 그대로 쓸 수 있게 되는 거고, 이제 양쪽이 서로 대칭키를 갖게 됐으니 이를 통해 서로 데이터들을 암호화하여 통신할 수 있게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;777&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6piFW/btstWIEtmYo/gZap8oFCVpxSdwQX492z30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6piFW/btstWIEtmYo/gZap8oFCVpxSdwQX492z30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6piFW/btstWIEtmYo/gZap8oFCVpxSdwQX492z30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6piFW%2FbtstWIEtmYo%2FgZap8oFCVpxSdwQX492z30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;777&quot; height=&quot;270&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우선 클라이언트가 랜덤한 데이터를 만들어 서버로 보낸다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;772&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pgCQc/btst89fntcq/eyO8dWJTfZsSFYP1Dlwxk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pgCQc/btst89fntcq/eyO8dWJTfZsSFYP1Dlwxk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pgCQc/btst89fntcq/eyO8dWJTfZsSFYP1Dlwxk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpgCQc%2Fbtst89fntcq%2FeyO8dWJTfZsSFYP1Dlwxk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;772&quot; height=&quot;303&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 서버도 랜덤한 데이터와 함께 CA에서 발급해준 인증서를 보낸다(이 인증서는 해당 CA의 개인키로 암호화돼있다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nzFGm/btstY4NmQu9/zNxbmgcgBHrafsuodVRSB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nzFGm/btstY4NmQu9/zNxbmgcgBHrafsuodVRSB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nzFGm/btstY4NmQu9/zNxbmgcgBHrafsuodVRSB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnzFGm%2FbtstY4NmQu9%2FzNxbmgcgBHrafsuodVRSB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;457&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 브라우저에 내장된 CA목록들을 통해 해당 CA의 공개키로 인증서를 복호화한다. 어떤 공개키로도 복호화가 안되면 당연히 서버 쪽을 신뢰할 수 없는 거고, 복호화됐다면 인증서를 보낸 서버를 공인된 서버라고 신뢰할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLQxEK/btst83MZIIm/Jat9MeWOpFdykK5eHCigAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLQxEK/btst83MZIIm/Jat9MeWOpFdykK5eHCigAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLQxEK/btst83MZIIm/Jat9MeWOpFdykK5eHCigAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLQxEK%2Fbtst83MZIIm%2FJat9MeWOpFdykK5eHCigAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;879&quot; height=&quot;466&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한 해당 인증서는 서버의 공개키를 가지고 있어서, 복호화에 성공했다면 요로코롬 서버의 공개키를 얻게 되는 거다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;828&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6YQMR/btstX3VsdPt/lQIsvRp7Vk7LzkKUC7h2mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6YQMR/btstX3VsdPt/lQIsvRp7Vk7LzkKUC7h2mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6YQMR/btstX3VsdPt/lQIsvRp7Vk7LzkKUC7h2mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6YQMR%2FbtstX3VsdPt%2FlQIsvRp7Vk7LzkKUC7h2mK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;337&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;822&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzihsk/btstY3OsrEP/Xc1idSocYVlpCIeOfFg070/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzihsk/btstY3OsrEP/Xc1idSocYVlpCIeOfFg070/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzihsk/btstY3OsrEP/Xc1idSocYVlpCIeOfFg070/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzihsk%2FbtstY3OsrEP%2FXc1idSocYVlpCIeOfFg070%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;822&quot; height=&quot;359&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;359&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/그 외 필요한 지식</category>
      <category>https</category>
      <category>개인키</category>
      <category>공개키</category>
      <category>대칭키</category>
      <category>비대칭키</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/119</guid>
      <comments>https://jofestudio.tistory.com/119#entry119comment</comments>
      <pubDate>Thu, 14 Sep 2023 22:53:19 +0900</pubDate>
    </item>
    <item>
      <title>Jenkins와 GitHub, SonarQube 연동해서 CI / CD 구축하기 (with DockerHub)</title>
      <link>https://jofestudio.tistory.com/118</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이전에 로컬에서 도커 이미지를 빌드하고 이를 도커 허브에 올린 뒤 ec2에서 이미지를 풀하고 컨테이너를 돌리는 식으로 배포해봤었는데, 새로운 기능을 추가하고 수정도 하면 이 과정을 하나하나 하기는 귀찮다. 그래서 jenkins라는 오픈소스를 활용해&amp;nbsp;빌드하고 테스트, 배포하는 과정을 자동화해본다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 나름대로의 코드 품질 개선과 혹시 모를 보안 위협에 대비하기 위해 sonarqube라는 코드 정적 분석 툴을 사용할 건데, 이는 sonarcloud라는 서비스를 통해 간단하게 도입해볼 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2702&quot; data-origin-height=&quot;1474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/66MZz/btstRK2YweD/lV5s5CQJkMPKB2QTOLCzik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/66MZz/btstRK2YweD/lV5s5CQJkMPKB2QTOLCzik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/66MZz/btstRK2YweD/lV5s5CQJkMPKB2QTOLCzik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F66MZz%2FbtstRK2YweD%2FlV5s5CQJkMPKB2QTOLCzik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2702&quot; height=&quot;1474&quot; data-origin-width=&quot;2702&quot; data-origin-height=&quot;1474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;pull request가 생기면 sonarqube(sonarcloud)에서 코드 정적 분석을 한다.&lt;/li&gt;
&lt;li&gt;main브랜치에 푸시가 발생하면(merge) 도커로 띄운 젠킨스로 웹훅(webhook)을 보낸다&lt;/li&gt;
&lt;li&gt;웹훅을 수신하면, jenkins쪽에서 지정된 깃헙 리포지토리의 코드를 땡겨온다 (+ application.properties 설정)&lt;/li&gt;
&lt;li&gt;이를 gradle을 통해 빌드하고(jar)&lt;/li&gt;
&lt;li&gt;이를 docker image로 빌드한 뒤 도커 허브에 푸쉬.&lt;/li&gt;
&lt;li&gt;실제 서비스가 돌아갈 ec2에서 도커 허브에 푸쉬된 이미지를 땡겨와 돌리게끔 하면 된다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 때 단순히 gradle로 빌드한 jar를 서비스를 돌릴 ec2로 보낼 수도 있지만(scp등으로) 멘토님 가라사대 요즘은 도커 모르면 안 된다라고 하셔서, 도커에 익숙해질 겸 도커 이미지를 만들고 이를 전달하는 식으로 가기로 했다. 그러면 도커로 띄운 젠킨스에서 도커 이미지를 만들어내야 해서, 도커 내에 도커를 설치해야 하니 이 점 유의&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;0. sonarcloud 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;깃헙 계정으로 가입해서 설정만 딸깍딸깍 누르면 된다. sonarcloud를 설치할 레포지토리들을 선택하면 지가 알아서 설치되고, 이후 pull request를 날리면 지가 알아서 다음과 같이 분석 결과를 남겨준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-14 오전 11.28.39.png&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;1508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAlPed/btstXZEzRG7/DcYYYvGPNaPvj2lBTT7KSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAlPed/btstXZEzRG7/DcYYYvGPNaPvj2lBTT7KSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAlPed/btstXZEzRG7/DcYYYvGPNaPvj2lBTT7KSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAlPed%2FbtstXZEzRG7%2FDcYYYvGPNaPvj2lBTT7KSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1866&quot; height=&quot;1508&quot; data-filename=&quot;스크린샷 2023-09-14 오전 11.28.39.png&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;1508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. ec2 띄우고 docker 설치 및 swap 메모리 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 ec2는 public subnet에 띄웠다. private subnet에 띄우면 깃헙 쪽에서 웹훅을 보낼 때 직접 받을 수 없기 때문에, 편의를 위해 그리 한 것임.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아 그리고 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;EBS볼륨을 20GB&lt;/b&gt;&lt;/span&gt;로 해줬다! 기본인 8GB로 해줬다가 용량이 없다고 하는 문제가 생겼었기 때문.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 ec2는 ubuntu로 띄웠고, 공식 매뉴얼에 나온 대로 도커를 설치했다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.docker.com/engine/install/ubuntu/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694533788458&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Install Docker Engine on Ubuntu&quot; data-og-description=&quot;Jumpstart your client-side server applications with Docker Engine on Ubuntu. This guide details prerequisites and multiple methods to install.&quot; data-og-host=&quot;docs.docker.com&quot; data-og-source-url=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; data-og-url=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iiS9o/hyTSDjhHxK/Hl9K4dUmHjTSW4FO3JZ56K/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/vPDZq/hyTSAtkuMV/BtpRwrIUbo5TF6vUlGGsKK/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.docker.com/engine/install/ubuntu/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iiS9o/hyTSDjhHxK/Hl9K4dUmHjTSW4FO3JZ56K/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/vPDZq/hyTSAtkuMV/BtpRwrIUbo5TF6vUlGGsKK/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Install Docker Engine on Ubuntu&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jumpstart your client-side server applications with Docker Engine on Ubuntu. This guide details prerequisites and multiple methods to install.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 뒤 swap 메모리를 설정해줬다. 나같은 경우 ec2를 프리티어로 해서 젠킨스를 띄울 건데, 이 때 프리티어는 메모리가 부족해서 멈추는 경우들이 있다고 한다. swap메모리를 설정해 이를 방지할 수 있다고 해서, 설정해줬다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(swap 메모리 : 메모리의 부족한 부분을 하드디스크로 대체하는 것)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아래 공식문서 글을 참고하면 된다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://repost.aws/ko/knowledge-center/ec2-memory-swap-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://repost.aws/ko/knowledge-center/ec2-memory-swap-file&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694544411987&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;스왑 파일을 사용하여 Amazon EC2 인스턴스의 스왑 공간으로 메모리 할당&quot; data-og-description=&quot;Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스에서 스왑 파일로 사용할 메모리를 할당하려고 합니다. 어떻게 해야 하나요?&quot; data-og-host=&quot;repost.aws&quot; data-og-source-url=&quot;https://repost.aws/ko/knowledge-center/ec2-memory-swap-file&quot; data-og-url=&quot;https://repost.aws/ko/knowledge-center/ec2-memory-swap-file&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HPfK8/hyTSzgUFIC/CWpEmQclYlDlYL3ILW9Dn0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://repost.aws/ko/knowledge-center/ec2-memory-swap-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://repost.aws/ko/knowledge-center/ec2-memory-swap-file&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HPfK8/hyTSzgUFIC/CWpEmQclYlDlYL3ILW9Dn0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;스왑 파일을 사용하여 Amazon EC2 인스턴스의 스왑 공간으로 메모리 할당&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스에서 스왑 파일로 사용할 메모리를 할당하려고 합니다. 어떻게 해야 하나요?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;repost.aws&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. jenkins 이미지 풀 땡기고 컨테이너로 돌리기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694501679223&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker pull jenkins/jenkins:lts&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694501645203&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run --name jenkins -p 8080:8080 -p 50000:50000 -d -v /var/run/docker.sock:/var/run/docker.sock -v jenkins_home:/var/jenkins_home -u root jenkins/jenkins:lts&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;--name : 컨테이너의 이름을 지정(여기서는 jenkins라는 이름으로)&lt;/li&gt;
&lt;li&gt;-p : 컨테이너를 띄우는 호스트의 포트와 컨테이너의 포트를 매핑하는 것(즉 호스트의 8080과 컨테이너의 8080이 매핑) 참고로 jenkins 웹 인터페이스가 8080으로 열리고, jenkins 슬레이브 에이전트가 50000으로 열린다&lt;/li&gt;
&lt;li&gt;-d : 컨테이너를 백그라운드에서 실행. 이를 통해 터미널로 다른 작업이 가능&lt;/li&gt;
&lt;li&gt;-v : 호스트와 컨테이너를 볼륨 마운트. 이를 통해 컨테이너와 호스트 머신 간에 데이터를 공유할 수 있음&lt;/li&gt;
&lt;li&gt;-v /var/run/docker.sock:/var/run/docker.sock -&amp;gt; 컨테이너 내부에서 호스트머신의 docker 데몬과 상호작용하게끔 하는 것.&lt;/li&gt;
&lt;li&gt;-v jenkins_home:/var/jenkins_home -&amp;gt; jenkins 설정과 데이터를 영구적으로 저장하기 위함&lt;/li&gt;
&lt;li&gt;-u root : 컨테이너 내에서 jenkins를 root사용자로 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 뒤 다음 커맨드를 통해 docker.sock파일의 권한을 수정해준다(다른 사용자도 접근 가능하게끔). 참고로 컨테이너 밖에서 실행해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1694539078312&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo chmod 666 /var/run/docker.sock&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 docker.sock은 도커 데몬과 컨테이너 간의 통신을 위한 소켓 파일이다.(그래서 ls -l로 보면 파일타입이 s가 나옴) 이를 통해 컨테이너 내부에서도 호스트 머신의 docker데몬과 통신 가능하며, 이를 통해 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;컨테이너 내부에서도 docker커맨드를 쓸 수 있게 되는 것&lt;/span&gt;. 컨테이너 내에서도 docker데몬에 대한 권한을 부여하기 위해 위 커맨드를 통해 권한을 주는 거라고 생각하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 컨테이너 내 도커 설치&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;엇? 컨테이너 내에서도 호스트의 도커 데몬과 통신할 수 있게끔 한 거면 도커를 설치할 필요가 없지 않나? (나는 도알못이기 때문..) 왜 도커를 설치하지?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이럴 거면 docker run을 할 때 굳이 -v를 해서 docker.sock을 컨테이너로 마운트한 이유가 없지 않나? 그냥 컨테이너에서 도커 깔았으면 거기서 자체적으로 도커 데몬 쓰면 되지..(참고로 이를 Docker in Docker, DinD라고 부르고 소켓파일 마운트해서 호스트의 도커 데몬을 쓰는 방식을 Docker ouf of docker, Dood라고 부른다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;간단하다. 제 아무리 호스트 꺼여도, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;어찌됐건 도커 데몬과 통신하려면 도커 클라이언트가 필요&lt;/span&gt;하기 때문에 도커를 설치하는거다. 다만 데몬을 호스트 꺼를 사용한다는 것. (그냥 DinD로 컨테이너 내에서 자체 도커 데몬을 써도 되나, 보안상 이를 권하지 않는다고 한다. 무려 도커공식피셜임. 참고로 도커를 요로코롬 컨테이너 내에 깔아도 docker.sock은 호스트와 마운트된 걸 사용하게 된다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그림으로 표현하면 아래와 같은 형태가 되겠다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;714&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IsPZh/btstNBdkDvh/ZqeYGVyF3KueUOHBrIIkR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IsPZh/btstNBdkDvh/ZqeYGVyF3KueUOHBrIIkR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IsPZh/btstNBdkDvh/ZqeYGVyF3KueUOHBrIIkR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIsPZh%2FbtstNBdkDvh%2FZqeYGVyF3KueUOHBrIIkR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;374&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;찾아보니 나는 젠킨스 이미지를 바로 컨테이너로 띄우면서 docker.sock을 마운트하고 컨테이너 내에 도커를 설치한 반면, 젠킨스 이미지를 베이스로 도커를 설치하는 도커이미지를 만든 다음(즉 젠킨스를 한 번 더 포장한 이미지인데 도커가 설치돼있는 이미지가 된다) 컨테이너 띄울 때 docker.sock을 마운트하는 방식도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;암튼.. 컨테이너 내에 도커를 설치해본다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우선 docker exec을 해서 컨테이너 내부로 들어간 다음, 위에서 한 것과 똑같은 가이드북대로 설치했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 중간에 apt-get update를 하니 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;The repository 'https://download.docker.com/linux/ubuntu bookworm Release' does not have a Release file&lt;/span&gt;이라는 오류가 났었다. 이는 아래 글을 참고해 /etc/apt/sources.list.d/docker.list를 지운 뒤 실행함으로써 해결했다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(/etc/apt/sources.list.d : 이&amp;nbsp;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;디렉토리에 있는 파일들은 APT 패키지 관리자에게 소프트웨어 패키지 저장소를 추가하는 데 사용됨)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;(docker.list : &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;Docker 패키지를 제공하는 저장소를 가리키며, 이 저장소는 Docker 엔진과 관련된 패키지를 제공)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dct-wonjung.tistory.com/entry/apt-update-404-not-found&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dct-wonjung.tistory.com/entry/apt-update-404-not-found&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694538451726&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;apt update 404 Not Found 에러 해결 방법 - The repository does not have a Release file.&quot; data-og-description=&quot;TmaxOS21에서 docker를 설치하려고 했더니 설치는 안되고 apt update를 할 때마저 아래와 같은 에러가 발생했다. $ sudo apt update 기존:1 http://tos-repo.tmaxos.com/tmax nabi InRelease 기존:2 http://tos-repo.tmaxos.com/tmax t&quot; data-og-host=&quot;dct-wonjung.tistory.com&quot; data-og-source-url=&quot;https://dct-wonjung.tistory.com/entry/apt-update-404-not-found&quot; data-og-url=&quot;https://dct-wonjung.tistory.com/entry/apt-update-404-not-found&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cYZskV/hyTSCraUsX/VBKJy8oQh8qmAcZvPibMxK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bThO0m/hyTVXHcnQp/leG0AM0em9JkPXfTskkpAK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://dct-wonjung.tistory.com/entry/apt-update-404-not-found&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dct-wonjung.tistory.com/entry/apt-update-404-not-found&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cYZskV/hyTSCraUsX/VBKJy8oQh8qmAcZvPibMxK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bThO0m/hyTVXHcnQp/leG0AM0em9JkPXfTskkpAK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;apt update 404 Not Found 에러 해결 방법 - The repository does not have a Release file.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TmaxOS21에서 docker를 설치하려고 했더니 설치는 안되고 apt update를 할 때마저 아래와 같은 에러가 발생했다. $ sudo apt update 기존:1 http://tos-repo.tmaxos.com/tmax nabi InRelease 기존:2 http://tos-repo.tmaxos.com/tmax t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dct-wonjung.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 또 이번엔 &lt;span style=&quot;background-color: #ffffff; color: #17191e; text-align: left;&quot;&gt;sudo apt-get install docker-ce docker-ce-cli containerd.io (이하 생략) 커맨드에서 &lt;span style=&quot;background-color: #ffffff; color: #17191e; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Package docker-ce is not available, but is referred to by another package. This may mean that the package is missing, has been obsoleted, or is only available from another source&lt;/span&gt;라는 메시지와 함께 &lt;/span&gt;오류가 났다. 아까 docker.list를 삭제했던게 원인인 듯 한데.. 문제는 얘를 삭제안하면 다른 오류가 계속 나던 상황이라.. 해결방법을 찾아보다가, docker.list를 지운 상태에서 아래 커맨드를 실행하니 설치가 잘 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694538869305&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -fsSL get.docker.com -o get-docker.sh

bash get-docker.sh // 다운받은 쉘 스크립트 파일 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하니 설치가 됐다. curl을 통해 get.docker.com으로부터 도커를 설치하는 스크립트를 get-docker.sh란 파일로 다운받은 뒤 이를 실행하는 커맨드다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. application.properties 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;내 프로젝트는 .gitignore를 통해 application.properties를 깃 관리 대상에서 제외했다. 따라서 나중에 젠킨스에서 깃헙 리포지토리에 있는 코드를 땡겨가도 그래도 빌드해 사용한다면 문제가 생길 것이다. application.properties를 주입하는 방법은 여러 가지가 있겠지만, 나는 컨테이너의 /home/env밑에 application.properties를 두고 나중에 젠킨스에서 이걸 가져다 쓰는 식으로 구성했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694537422224&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it jenkins bash // 컨테이너 내부로 들어가기

cd /home

mkdir env // /home밑에 env 디렉토리 생성

cd env

echo &quot;{application.properties 내용}&quot; &amp;gt;&amp;gt; application.properties&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이런 방식으로 만들었었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;여까지 했다면, 이제 젠킨스라는 툴을 사용해 ci cd 파이프라인을 구축할 준비가 된 거다. 젠킨스 설정은 젠킨스에서 제공하는 웹 인터페이스(즉 사이트)를 통해 가능하니, 웹사이트로 들어가 설정을 진행해본다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 젠킨스 웹사이트 접속 (ec2의 퍼블릭ip:8080)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-09 오전 1.56.37.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5TtYK/btstqDKp8Xj/o64Pwn2gHgybMWivi9NmD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5TtYK/btstqDKp8Xj/o64Pwn2gHgybMWivi9NmD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5TtYK/btstqDKp8Xj/o64Pwn2gHgybMWivi9NmD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5TtYK%2FbtstqDKp8Xj%2Fo64Pwn2gHgybMWivi9NmD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;741&quot; height=&quot;682&quot; data-filename=&quot;스크린샷 2023-09-09 오전 1.56.37.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;956&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;docker exec -it {컨테이너명} bash 같은 걸로 컨테이너 안에 들어가서 저 경로로 cat찍으면 패스워드 볼 수 있으니 그리 하면 된다. (사실 엄밀히 말하면 들어간다기보다는 컨테이너에서 작업할 수 있게 해주는 커맨드라고 보는게 맞다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-09 오전 1.57.01.png&quot; data-origin-width=&quot;1129&quot; data-origin-height=&quot;935&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQSZM/btstvO5tHrA/nDthKAAWbsxJbuu2oPpz41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQSZM/btstvO5tHrA/nDthKAAWbsxJbuu2oPpz41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQSZM/btstvO5tHrA/nDthKAAWbsxJbuu2oPpz41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQSZM%2FbtstvO5tHrA%2FnDthKAAWbsxJbuu2oPpz41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;823&quot; height=&quot;682&quot; data-filename=&quot;스크린샷 2023-09-09 오전 1.57.01.png&quot; data-origin-width=&quot;1129&quot; data-origin-height=&quot;935&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;추천해주는 플러그인 설치한 뒤, 회원가입 페이지(?)같은 게 나온다. 자신의 플젝에 맞게 설정하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 젠킨스 설정&amp;nbsp; - jdk, gradle, github &amp;amp; dockerhub token&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;jar 빌드를 위해 jdk와 gradle을 설정해줄 필요가 있고, 깃허브 리포지토리에 있는 코드들을 땡겨와야 하기 때문에 github token이 필요하며, 도커 이미지를 도커허브에 올리기 위해 dockerhub token도 필요하다. 우선 jdk와 gradle설정부터 시작해본다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에서 jdk11버전을 썼다면 모를까, 나는 17버전을 쓰기 때문에 따로 설정을 해줘야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;jenkins관리 - tools로 가서 jdk를 설정해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-12 오전 1.23.55.png&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7SU4S/btstxqqIqMy/yC5xWRWknr8weuTdmYj5EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7SU4S/btstxqqIqMy/yC5xWRWknr8weuTdmYj5EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7SU4S/btstxqqIqMy/yC5xWRWknr8weuTdmYj5EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7SU4S%2FbtstxqqIqMy%2FyC5xWRWknr8weuTdmYj5EK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1587&quot; height=&quot;759&quot; data-filename=&quot;스크린샷 2023-09-12 오전 1.23.55.png&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;759&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 설정은 아래 글을 참고했었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694534817958&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;[Jenkins] pipeline jdk 설정&quot; data-og-description=&quot;[Jenkins] pipeline jdk 설정 작업 순서 JDK 설정 pipeline에서 특정 버전의 JDK 사용하기 1. JDK 설정 Jenkins 관리 &amp;gt; Global Tool Configuration &amp;gt; JDK 항목 &amp;gt; Add JDK https://stackoverflow.com/questions/55243120/jenkins-add-jdk-11-to-jdk-l&quot; data-og-host=&quot;royleej9.tistory.com&quot; data-og-source-url=&quot;https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95&quot; data-og-url=&quot;https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bvEliv/hyTV31IA3Z/J0KCytoTBeHqmEm49VZJQk/img.png?width=800&amp;amp;height=519&amp;amp;face=0_0_800_519,https://scrap.kakaocdn.net/dn/bNUOc4/hyTVTrfGdb/loiLBTaJb2IaHVgfxbFcsk/img.png?width=800&amp;amp;height=519&amp;amp;face=0_0_800_519,https://scrap.kakaocdn.net/dn/b5tOt6/hyTStgGpO0/bzJi9QhfE5NmokLLoxi7ok/img.png?width=1025&amp;amp;height=666&amp;amp;face=0_0_1025_666&quot;&gt;&lt;a href=&quot;https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bvEliv/hyTV31IA3Z/J0KCytoTBeHqmEm49VZJQk/img.png?width=800&amp;amp;height=519&amp;amp;face=0_0_800_519,https://scrap.kakaocdn.net/dn/bNUOc4/hyTVTrfGdb/loiLBTaJb2IaHVgfxbFcsk/img.png?width=800&amp;amp;height=519&amp;amp;face=0_0_800_519,https://scrap.kakaocdn.net/dn/b5tOt6/hyTStgGpO0/bzJi9QhfE5NmokLLoxi7ok/img.png?width=1025&amp;amp;height=666&amp;amp;face=0_0_1025_666');&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;[Jenkins] pipeline jdk 설정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Jenkins] pipeline jdk 설정 작업 순서 JDK 설정 pipeline에서 특정 버전의 JDK 사용하기 1. JDK 설정 Jenkins 관리 &amp;gt; Global Tool Configuration &amp;gt; JDK 항목 &amp;gt; Add JDK https://stackoverflow.com/questions/55243120/jenkins-add-jdk-11-to-jdk-l&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;royleej9.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;download url&lt;/b&gt; : 내가 원하는 버전의 jdk를 다운받는 경로&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_x64_linux_hotspot_17.0.8.1_1.tar.gz&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;subdirectory&lt;/b&gt; : url을 통해 다운된 파일을 압축 해제 했을 때 나오는 폴더 이름&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;jdk-17.0.8.1+1&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;gradle 설정도 jenkins관리 - tools에서 해주며, 플젝에서 쓰는 spring boot에서 사용중인 버전과 똑같은 놈으로 하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;본인 스프링부트 프로젝트의 gradle&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt; wrapper &amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;gradle&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;-wrapper.properties에서 확인 가능하다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;888&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PybQy/btstG74rzC4/cKABlNgaF93bgPc0SbUPUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PybQy/btstG74rzC4/cKABlNgaF93bgPc0SbUPUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PybQy/btstG74rzC4/cKABlNgaF93bgPc0SbUPUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPybQy%2FbtstG74rzC4%2FcKABlNgaF93bgPc0SbUPUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;658&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 jenkins관리 - Credentials로 가서 깃허브 토큰을 등록한다. 깃허브 토큰을 발급받는 과정은 생략한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HRlul/btstOjbz2AJ/w3rshTwHsukOVkHKuKEXm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HRlul/btstOjbz2AJ/w3rshTwHsukOVkHKuKEXm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HRlul/btstOjbz2AJ/w3rshTwHsukOVkHKuKEXm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHRlul%2FbtstOjbz2AJ%2Fw3rshTwHsukOVkHKuKEXm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;839&quot; height=&quot;790&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 때 kind를 secret text가 아닌 username with password로 해준다. git이 뭐 지원 안한다고 함&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;username : 깃허브 계정 (이메일)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;password : 깃헙 토큰&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;id : 본인이 식별할 수 있는 거 암거나 작성&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;마찬가지로 dockerhub token도 발급받은 뒤 똑같이 username with password로 등록해주면 된다. username은 도커허브 계정(아이디) 넣으면 되고, password에 토큰값 넣어주면 되고, id도 본인이 식별가능한 거 써주면 된다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 젠킨스 설정 - ssh 관련 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;도커 이미지를 도커 허브에 푸시한 뒤, ssh로 서비스를 배포할 ec2에 접속해 도커허브로부터 이미지를 땡기는 작업을 하게 된다. 이를 위해 ssh를 사용하는데, 예전엔 Publish Over SSH라는 걸 썼던 듯 하나 지금은 못 쓰는 듯 하다. (새로운 openssh에서 ssh-rsa키 유형이 비활성화됐다고 한다. https://github.com/jenkinsci/publish-over-ssh-plugin/issues/247#issuecomment-1153768171)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 이제 젠킨스가 띄워진 ec2에 ssh키를 물리적으로 둘 수도 있을 텐데, 이건 사실상 현관문 바로 앞에 키를 두는 방식이니 좋지 않다. 찾아보니 ssh agent플러그인을 젠킨스에서 다운받아 쓰는 방법이 있어, 이를 쓰기로 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;jenkins관리 -&amp;gt; 플러그인 -&amp;gt; available plugins로 가서 ssh agent를 install한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doxrhB/btstRrVKoYj/VMWsBbit7IHDqIS9KnVKR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doxrhB/btstRrVKoYj/VMWsBbit7IHDqIS9KnVKR1/img.png&quot; data-alt=&quot;난 이미 설치해서 안 뜬다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doxrhB/btstRrVKoYj/VMWsBbit7IHDqIS9KnVKR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoxrhB%2FbtstRrVKoYj%2FVMWsBbit7IHDqIS9KnVKR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;871&quot; height=&quot;327&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;난 이미 설치해서 안 뜬다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 다음 아까 깃헙&amp;amp;도커허브 토큰을 등록했을 때처럼 ssh key정보를 등록한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-12 오후 7.14.11.png&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9g13N/btstNzmkOxw/al8xEfuCKudzXDq2VXFpKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9g13N/btstNzmkOxw/al8xEfuCKudzXDq2VXFpKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9g13N/btstNzmkOxw/al8xEfuCKudzXDq2VXFpKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9g13N%2FbtstNzmkOxw%2Fal8xEfuCKudzXDq2VXFpKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1934&quot; height=&quot;1712&quot; data-filename=&quot;스크린샷 2023-09-12 오후 7.14.11.png&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;cat으로 ec2의 ssh key(pem파일)을 본 다음, 내용물을 적어주면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. 깃허브 웹훅 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;리포지토리 세팅 -&amp;gt; Webhooks로 가서 http://젠킨스서버ip:8080/github-webhook/을 등록한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-09 오전 2.08.18.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I18pC/btstq7YMvsb/9j9btSFFKk2i5alNXEOqi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I18pC/btstq7YMvsb/9j9btSFFKk2i5alNXEOqi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I18pC/btstq7YMvsb/9j9btSFFKk2i5alNXEOqi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI18pC%2Fbtstq7YMvsb%2F9j9btSFFKk2i5alNXEOqi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1126&quot; height=&quot;718&quot; data-filename=&quot;스크린샷 2023-09-09 오전 2.08.18.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 깃허브 쪽에서 보내는 웹훅을 젠킨스서버가 잘 받기 위해, 젠킨스 서버에 대한 보안그룹에서 깃허브 쪽에서 오는 인바운드들이 열려 있어야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://api.github.com/meta&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://api.github.com/meta&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;요기에서 현재 깃허브에서 웹훅을 보낼 때 쓰는 ip들을 확인 가능하다. 현재&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;prolog&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;&quot;hooks&quot;: [
    &quot;192.30.252.0/22&quot;,
    &quot;185.199.108.0/22&quot;,
    &quot;140.82.112.0/20&quot;,
    &quot;143.55.64.0/20&quot;,
    &quot;2a0a:a440::/29&quot;,
    &quot;2606:50c0::/32&quot;
  ],&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;요 ip들에서 8080으로 오는 인바운드들을 다 열어주면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;9. 파이프라인 작성&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;749&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCdckP/btstH3BVdxF/FM2rJBomHhMElcQF624r5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCdckP/btstH3BVdxF/FM2rJBomHhMElcQF624r5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCdckP/btstH3BVdxF/FM2rJBomHhMElcQF624r5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCdckP%2FbtstH3BVdxF%2FFM2rJBomHhMElcQF624r5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;749&quot; height=&quot;691&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;General - GitHub project에 리포지토리 url(.git으로 끝나는..)을 입력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs8Mlj/btstRukEgJz/oyZfwKv5S812NTicq901U1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs8Mlj/btstRukEgJz/oyZfwKv5S812NTicq901U1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs8Mlj/btstRukEgJz/oyZfwKv5S812NTicq901U1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs8Mlj%2FbtstRukEgJz%2FoyZfwKv5S812NTicq901U1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1179&quot; height=&quot;601&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;601&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 build triggers에서 다음과 같이 체크해준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-13 오전 4.20.51.png&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PpELe/btstMeCUtkk/O87I6QSsyM1ZvwKnY8TVyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PpELe/btstMeCUtkk/O87I6QSsyM1ZvwKnY8TVyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PpELe/btstMeCUtkk/O87I6QSsyM1ZvwKnY8TVyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPpELe%2FbtstMeCUtkk%2FO87I6QSsyM1ZvwKnY8TVyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;470&quot; height=&quot;311&quot; data-filename=&quot;스크린샷 2023-09-13 오전 4.20.51.png&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;311&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;난 다음과 같이 작성해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1694545867301&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    environment { 
        repository = &quot;{도커허브계정}/{레포지토리 이름}&quot; // 이메일말고 아이디
        DOCKERHUB_CREDENTIALS = credentials('등록해둔 도커허브토큰id') // jenkins에 등록해 놓은 docker hub credentials 이름
        dockerImage = '' 
    }
    
    tools {
        jdk &quot;{jdk이름}&quot; //아까 jdk등록할 때 name으로 준 값
    }
    
    agent any 
    
    stages {
        stage('GitHub Repository Clone') { 
            steps { // url의 main branch 내용을 credential을 통해 땡겨온다
              
                git branch: 'main', credentialsId: '{등록해둔 깃헙토큰id}', url: '{깃헙 리포지토리 주소(.git으로 끝나는)}'
              
            }
        }
        
        stage('Set Application.properties') {
            steps {
                sh '''
                    if [ ! -d ./src/main/resources ]; then
                        mkdir ./src/main/resources
                    else
                        echo &quot;resources directory alreay exist&quot;
                    fi
                    
                    if [! -e ./src.main/resources/application.properties ]; then
                        rm ./src/main/resources/application.properties
                    fi
                    
                    cp /home/env/application.properties /var/jenkins_home/workspace/{지어준 파이프라인 이름}/src/main/resources
                    
                    cat ./src/main/resources/application.properties
                    '''
                
            }
        }
        
        stage('Build jar') {
            steps{
                sh '''
                    echo 'start bootJar'
                    echo $JAVA_HOME
                    ./gradlew clean bootJar
                    '''
            }
        }
        
        stage('Docker build') {
            steps {
                sh '''
                    docker build -t $repository:latest .
                    '''
            }
        }
        
        stage('Dockerhub Login'){
            steps {
                sh '''
                    echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin
                    '''
            }
        }
        
        stage('Push Docker image') {
            steps {
                sh '''
                    docker push $repository:latest
                    '''
            }
            
            post {
                success {
                    echo 'push success'
                }
                
                failure {
                    echo 'push failure'
                }
            }
        }
        
        stage('Run New Docker Container'){
            steps {
                sshagent(credentials: ['ssh-key']) {
                    sh '''
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} 'sudo docker ps -a'
                    
                    // 기존에 돌아가던 컨테이너 삭제
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} '
                        if [ &quot;$(sudo docker ps -q --filter name={삭제할 컨테이너명}&quot; ]; then
                            sudo docker rm -f $(sudo docker ps -aq --filter name={삭제할 컨테이너명})
                        else
                            echo &quot;no there containers&quot;
                        fi
                    '
                    
                    // 기존 이미지 삭제
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} '
                        if [ &quot;$(sudo docker images -q)&quot; ]; then
                            sudo docker rmi $(sudo docker images -q)
                        else
                            echo &quot;no there images&quot;
                        fi
                    '
                    
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} 'sudo docker pull {아까 푸시한 이미지명}'
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} 'sudo docker run -d --name={컨테이너명} -p 8080:8080 {풀받은 이미지명}'
                    
                    '''
                }
            }
        }
        
        stage('Clean Docker Image') {
            steps {
                sh '''
                    docker rmi $repository:latest
                    '''
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 : docker image -q 또는 docker ps -q -&amp;gt; 아이디값만 보게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2716&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NZdUD/btstMlWUEES/S1tj7FKvKERDrK5CPyuCr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NZdUD/btstMlWUEES/S1tj7FKvKERDrK5CPyuCr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NZdUD/btstMlWUEES/S1tj7FKvKERDrK5CPyuCr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNZdUD%2FbtstMlWUEES%2FS1tj7FKvKERDrK5CPyuCr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2716&quot; height=&quot;898&quot; data-origin-width=&quot;2716&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같이 파이프라인이 잘 돌아가는 걸 확인할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌보면 남들 다 하는 ci cd 구성이지만.. 그래도 처음 해보면서 삽질 여러가지 하면서 배운 게 많은 것 같다. DinD나 DooD도 있고, 특히 쉘 스크립트 작성을 꽤나 해보게 됐다ㅋㅋㅋㅋ.. jar로 빌드하고, 도커 이미지 빌드하고, 도커허브에 푸쉬하고, 서비스 띄울 ec2에선 기존 컨테이너랑 이미지 삭제한 뒤에 새로 풀받아서 돌리고.. 한 스텝 한 스텝 성공할 때마다 왠지 모를 희열이 느껴졌다.&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;다만 아직 고려해야할 부분들이 더 많다. 당장 도커 이미지만 봐도 일단 전부다 latest를 달아서 하게끔 했는데, 이것보다는 빌드별로 번호를 매겨서 붙여주는게 더 나을 거다. 또한 지금 구축한 것대로라면 기존에 돌아가던 컨테이너를 내리고 새로 이미지를 받은 뒤 컨테이너를 띄우는 동안은 서비스가 중단된다!! 즉 무중단 배포도 고려해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 개발을 해야 할 게 남아 있는 만큼, 그리고 https 구축도 해야 하는 만큼 일단 그것들에 먼저 신경을 쓰고 해야 할 듯 하다..&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS/CI &amp;amp; CD</category>
      <category>CD</category>
      <category>CI</category>
      <category>docker</category>
      <category>dood</category>
      <category>github</category>
      <category>jenkins</category>
      <category>Webhook</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/118</guid>
      <comments>https://jofestudio.tistory.com/118#entry118comment</comments>
      <pubDate>Wed, 13 Sep 2023 04:30:12 +0900</pubDate>
    </item>
    <item>
      <title>Nginx 설정 - server 블록의 server_name에 대한 실험과 삽질 기록</title>
      <link>https://jofestudio.tistory.com/117</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(우분투 기준) nginx설치 후 /etc/nginx로 가보면 nginx.conf라는 파일이 있고, 여기엔 nginx의 설정 파일인 nginx.conf가 있다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693894206278&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
	# multi_accept on;
}

http {

	##
	# Basic Settings
	##

	sendfile on;
	tcp_nopush on;
	types_hash_max_size 2048;
	# server_tokens off;

	# server_names_hash_bucket_size 64;
	# server_name_in_redirect off;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	##
	# SSL Settings
	##

	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;

	##
	# Logging Settings
	##

	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	##
	# Gzip Settings
	##

	gzip on;

	# gzip_vary on;
	# gzip_proxied any;
	# gzip_comp_level 6;
	# gzip_buffers 16 8k;
	# gzip_http_version 1.1;
	# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

	##
	# Virtual Host Configs
	##
	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;

}


#mail {
#	# See sample authentication script at:
#	# http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
#	# auth_http localhost/auth.php;
#	# pop3_capabilities &quot;TOP&quot; &quot;USER&quot;;
#	# imap_capabilities &quot;IMAP4rev1&quot; &quot;UIDPLUS&quot;;
#
#	server {
#		listen     localhost:110;
#		protocol   pop3;
#		proxy      on;
#	}
#
#	server {
#		listen     localhost:143;
#		protocol   imap;
#		proxy      on;
#	}
#}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 http블록 안에 직접 server 블록을 작성해줌으로써 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;웹 서버에서 특정 도메인, 호스트 이름, 포트 또는 IP 주소와 관련하여 요청을 어떻게 처리할지를 정의할 수 있다. 또는 위에 적혀있듯이 /etc/nginx/sites-enabled나 /etc/nginx/conf.d에 작성한 conf파일에 적힌 내용을 include하는 식으로도 server 블록을 정의할 수 있다(즉 직접 nginx.conf안에 작성하든가, 아니면 외부에서 작성한 후 include로 끌고 올 수 있다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 server 블록은 각 블록별로 하나의 웹사이트 또는 가상 호스트(Virtual Host)를 의미한다. 즉 여러 개의 server 블록을 작성해 다수의 도메인을 하나의 서버에서 가동할 수 있는 것이고, 이는 다수의 도메인에 대해 하나의 서버에서 각기 다른 페이지를 서비스할 수 있다는 말이다. server 블록은 다음과 같이 작성 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693895563800&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80;
    server_name example.com;

    location / {
        # example.com에 대한 설정 ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;listen : nginx server가 클라이언트로부터 요청을 수신하는 방법을 정의. 걍 어느 ip 어느 포트로 올 건지를 말 그대로 listen한다는 것을 설정하는 부분&lt;/li&gt;
&lt;li&gt;server_name : 해당 server 블록이 처리할 도메인이나 호스트네임 또는 ip주소를 지정. 클라이언트로부터 request가 오면 header에 있는 Host값과 이 server_name값이 일치하면 해당 server블록을 활성화하는 것임. server_name을 없애고 listen에 example.com:80으로도 작성 가능함&lt;/li&gt;
&lt;li&gt;location 블록 : 특정 URL 경로 또는 패턴에 대한 요청을 처리하는 방법을 정의. 해당 경로에 대한 프록시 패스를 처리한다든지..그런 것들을 설정해줄 수 있다. 위 예제에선 example.com의 모든 요청(/이니까~)에 대한 모든 요청에 대해 location블록에 설정한 내용이 동작할 것임. 필요에 따라 한 server 블록에 여러 location블록을 둘 수 있음&lt;/li&gt;
&lt;li&gt;기타 다른 설정도 가능..&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;여기서 내가 처음에 헷갈렸던 건, &quot;server_name으로 해당 server블록이 처리할 도메인을 적으라는 것&quot;. 아니 그러면 naver 도메인같은 걸 여기 적어줘도 되는거야? 란 생각이 들었다. 그러면 말이 안 되지 않나? 저 문장대로면 브라우저에 naver치고 들어가는 걸 naver서버가 아니라 내가 띄운 nginx가 처리해야 한다는 거 아닌가?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그리고 애당초 난 처음엔 server_name을 프록시 대상의 도메인이나 ip주소를 적어줘야 하는 줄 알고 있었다.. (nginx를 리버스 프록시로 쓸 생각으로 설치했었고 자료를 찾아봤어서...) 그래서 nginx 처음 쓰는데 server_name에 프록시 대상으로 was의 주소(nginx를 웹서버, spring boot를 was로 사용해서 was의 주소를 적어줬었음)를 줬는데 잘만 동작하길래 내가 이해한 게 맞다 싶었다. 근데 알고 보니 server_name으로는 server블록이 처리할 도메인과 같이 웹서버에 관한 뭔가를 쓰는 거여서 뭐지? 싶던 거.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&quot;난 server_name으로 웹서버가 아닌 was에 대한 걸 적어줬는데 왜 프록시 패스가 잘 되는거지?&quot;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그래서 실험을 좀 했다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;우선 server 블록은 다음과 같이 1개만 작성된 상태&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;860&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfsFjo/btss9jShfMz/ksVsIKyk3WMssIKpizDNu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfsFjo/btss9jShfMz/ksVsIKyk3WMssIKpizDNu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfsFjo/btss9jShfMz/ksVsIKyk3WMssIKpizDNu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfsFjo%2Fbtss9jShfMz%2FksVsIKyk3WMssIKpizDNu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;278&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;server_name에 was(spring boot)의 사설 ip를 박아뒀었다. nginx는 public subnet에 띄웠었고, was랑 같은 vpc안에 있으니 사설 ip를 통해서도 접근이 가능하기 때문에, was의 사설 ip를 server_name에 박아줘도 된다고 생각했음. 그래서 proxy_pass에 server_name에 준 주소를 그대로 써먹은 모습을 확인 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;암튼 이렇게 두면, 잘 된다. nginx쪽에 request를 보내면 잘만 was쪽으로 request를 던져준다. 근데 왜 잘 되는 건지는 몰랐다. 일단 server_name에 10.0.1.88을 박아뒀으니 reqeust의 Host와 매칭되진 않을텐데? 근데 왜 프록시 패스가 잘 되지?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;바보같은 생각이지만(?) Host와 server_name이 매칭이 안 되면 10.0.1.88로 뭔지 모르겠지만 보내나,,? 싶었다. 그래서 server 블록을 하나 더 추가해줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.12.42.png&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TaLtP/btss8jZiDMF/cxckyngXMgtLvEAyEAKLH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TaLtP/btss8jZiDMF/cxckyngXMgtLvEAyEAKLH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TaLtP/btss8jZiDMF/cxckyngXMgtLvEAyEAKLH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTaLtP%2Fbtss8jZiDMF%2FcxckyngXMgtLvEAyEAKLH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;584&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.12.42.png&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하니, 안된다. nginx쪽으로 보낸 request가 was쪽으로 던져지지 않는다(즉 was에 request가 오지 않음). 매칭이 안 된다고 10.0.1.88로 무조건 보내는 건 아닌 듯 하다. 근데 이상하게 두 server 블록의 순서를 바꿔주면 됐었음. (이때까진 이유를 몰랐다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사실 이때까지도 server_name을 프록시 대상의 ip를 줘야 하지 않을까란 의심(?)을 버리지 못한 상태라,, server_name이랑 request의 Host가 정확히 어떻게 매칭이 되는거지라는 생각이 들었다.. server_name을 아예 nginx가 띄워져있는 ec2의 public ip를 주면 어떨까?란 생각이 들어서 바꿔봤고..실험해봤다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.12.59.png&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhVs22/btss73CmSPz/LE0zCLqdiYKp0bEJ0nsPH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhVs22/btss73CmSPz/LE0zCLqdiYKp0bEJ0nsPH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhVs22/btss73CmSPz/LE0zCLqdiYKp0bEJ0nsPH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhVs22%2Fbtss73CmSPz%2FLE0zCLqdiYKp0bEJ0nsPH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;876&quot; height=&quot;595&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.12.59.png&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;595&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ㅇㅇ. 이렇게 하니 nginx로 보낸 request가 was로 잘 전달된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;음 그러면 확실하게 server_name은 프록시 대상과는 아무런 연관이 없다. server_name과 request의 Host가 매칭돼야 하는거라면, nginx가 띄워진 ec2의 public ip를 server_name으로 줬을 때 해당 ip로 보내는 request의 Host와 매칭되는건 당연하다. 그러면 아까 들었던 의문으로 돌아가서.. server_name으로 아무 도메인(네이버 등)이나 막 줘도 request의 Host랑 매칭만 되면 동작하겠다는 생각이 들었다. 그러나 주소창에 naver를 치고 들어가면 당연히 내 nginx가 이를 캐치할 리는 없고..엇? nginx로 보내는 request에 담긴 Host값만 잘 조작하면 프록시 패스가 되겠네?란 생각이 들었다. 당장 실험에 옮기기로 했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.14.08.png&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1DtVG/btstg5dSIph/9PhzRgm5djYv6DBV04OYjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1DtVG/btstg5dSIph/9PhzRgm5djYv6DBV04OYjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1DtVG/btstg5dSIph/9PhzRgm5djYv6DBV04OYjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1DtVG%2Fbtstg5dSIph%2F9PhzRgm5djYv6DBV04OYjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;964&quot; height=&quot;591&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.14.08.png&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;server_name에 www.naver.com 입력하고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.17.33.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kw3Q6/btss9iy6j2c/T9tI6iqXtGJhFOmtaJANAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kw3Q6/btss9iy6j2c/T9tI6iqXtGJhFOmtaJANAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kw3Q6/btss9iy6j2c/T9tI6iqXtGJhFOmtaJANAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkw3Q6%2Fbtss9iy6j2c%2FT9tI6iqXtGJhFOmtaJANAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1060&quot; height=&quot;438&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.17.33.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;nginx로 보내는 request의 Host를 www.naver.com적어서 보내본다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;결과는..!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.18.25.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8lPYy/btstbeJHEIy/CagZyISYXIG7Gwfi6Bw01K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8lPYy/btstbeJHEIy/CagZyISYXIG7Gwfi6Bw01K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8lPYy/btstbeJHEIy/CagZyISYXIG7Gwfi6Bw01K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8lPYy%2FbtstbeJHEIy%2FCagZyISYXIG7Gwfi6Bw01K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1038&quot; height=&quot;276&quot; data-filename=&quot;스크린샷 2023-09-05 오전 3.18.25.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;response가 왔다는거 = nginx에서 was로 request를 던져줬다. 라는 것.. 즉 예측에 성공했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아하! server 블록을 여러 개 둔 뒤 블록별로 server_name을 이것저것 주면, 클라이언트에서 request의 Host를 직접 조작해서 보내는 것을 통해 이런저런 조작을 할 수 있구나~! 이렇게 활용하나보다~!&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;싶었으나! 이내 깨달았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&quot;하나의 ip주소에 여러 도메인을 연결한다면&quot;..!&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉..nginx는 물리적으로 하나의 콤퓨타 위에서 돌아가고 있으니, 해당 컴퓨터에 대한 하나의 ip로 요청을 보낼 건데.. 그 ip가 여러 도메인에 연결돼있다면, 우리 킹갓 nginx가 그 도메인들 별로 각기 다른 서비스를 제공해줄 수 있다는 거&lt;/b&gt;다. 뭐 클라이언트에서 직접 request의 Host를 직접 조작해서 보내는..그런 이상한 짓(?)은 안 하는거(물론 원한다면 할 순 있겠지만..)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 그러면 아까 server블록을 하나만 작성했는데 server_name이 request의 Host와 매칭이 안 됐는데도 동작했던 이유는 뭘까? 그리고 server블록을 여러 개 뒀었는데 순서에 따라 어떨 땐 동작하고 어떨 땐 동작하지 않던 이유는 뭘까?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;간단하다. Host와 매칭되는 server블록이 하나도 없으면, 기본으로 설정된 server 블록 또는 제일 처음 작성된 server 블록이 활성화된다&lt;/b&gt;고 한다. 그래서 그랬던 거..따라서 server 블록의 server_name은 필요에 따라 굳이 작성할 필요가 없다. (server블록이 하나만 있다거나 등등..)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이런 삽질과 구글링을 통해 배운 걸 정리하면..&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx설정은 nginx.conf를 편집해서 가능&lt;/li&gt;
&lt;li&gt;server블록에 대한 걸 설정하려면 -&amp;gt; nginx.conf에 직접 작성 또는 /etc/nginx/sites-enabled에 심볼릭 링크로 걸어둔 파일을 통해(nginx.conf에서 여깄는 것들을 include로 땡겨오니까)&lt;/li&gt;
&lt;li&gt;server_name은 nginx가 띄워진 ip 또는 그 ip에 묶인 도메인들을 작성. ip에 묶인 도메인이 여러 개면 각 도메인별로 다른 페이지 등을 제공 가능&lt;/li&gt;
&lt;li&gt;server 블록이 여러 개일때 request의 Host와 server_name이 매칭되는게 하나도 없으면 기본 또는 젤 처음 작성된 server블록을 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/Nginx</category>
      <category>nginx</category>
      <category>server</category>
      <category>server_name</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/117</guid>
      <comments>https://jofestudio.tistory.com/117#entry117comment</comments>
      <pubDate>Wed, 6 Sep 2023 03:32:37 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Elastic IP EC2에 부여하고 도메인과 연결하기</title>
      <link>https://jofestudio.tistory.com/116</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;public subnet에 띄운 ec2에 고정된 공인ip, 즉 elastic ip를 붙이고 이를 route 53에서 구입한 도메인과 연결해본다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Elastic IP 생성 및 ec2와 연결&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;1. Elastic IP를 생성한다. 생성하는 모습은 생략함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오후 4.10.49.png&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgaPaY/btsteS618xx/rhGUHTIXaQlNm314VpT28K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgaPaY/btsteS618xx/rhGUHTIXaQlNm314VpT28K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgaPaY/btsteS618xx/rhGUHTIXaQlNm314VpT28K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgaPaY%2FbtsteS618xx%2FrhGUHTIXaQlNm314VpT28K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2864&quot; height=&quot;178&quot; data-filename=&quot;스크린샷 2023-09-05 오후 4.10.49.png&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오후 3.55.47.png&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GHzyt/btss832PPzP/Bk43ZAANxgVjQl1XAZiBfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GHzyt/btss832PPzP/Bk43ZAANxgVjQl1XAZiBfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GHzyt/btss832PPzP/Bk43ZAANxgVjQl1XAZiBfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGHzyt%2Fbtss832PPzP%2FBk43ZAANxgVjQl1XAZiBfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;367&quot; data-filename=&quot;스크린샷 2023-09-05 오후 3.55.47.png&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;654&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;2. 생성한 elastic ip를 선택하고 작업 탭을 눌러 &quot;탄력적 IP 주소 연결&quot;을 누른다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오후 3.57.31.png&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;1500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQYhZA/btsteTZlQjU/UiLaWik9PiAsfTUOhB1c41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQYhZA/btsteTZlQjU/UiLaWik9PiAsfTUOhB1c41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQYhZA/btsteTZlQjU/UiLaWik9PiAsfTUOhB1c41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQYhZA%2FbtsteTZlQjU%2FUiLaWik9PiAsfTUOhB1c41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1652&quot; height=&quot;1500&quot; data-filename=&quot;스크린샷 2023-09-05 오후 3.57.31.png&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;1500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;리소스 유형 = 인스턴스고르고 연결할 ec2 고르면 된다. 프라이빗 주소는 그 인스턴스의 사설ip를 고르면 된다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;3. 그러면 해당 ec2의 상세화면에서 public ip로 elastic ip가 연결된 걸 볼 수 있다&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;route53에서 구입한 도메인과 elastic ip연결&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;1. 구입한 도메인의 호스팅 영역에 들어가서 &quot;레코드 생성&quot;을 누른다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오후 4.02.26.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l7SBr/btstfSMtzU9/DC6WGMNwvhDYE6176niMDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l7SBr/btstfSMtzU9/DC6WGMNwvhDYE6176niMDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l7SBr/btstfSMtzU9/DC6WGMNwvhDYE6176niMDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl7SBr%2FbtstfSMtzU9%2FDC6WGMNwvhDYE6176niMDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1956&quot; height=&quot;1280&quot; data-filename=&quot;스크린샷 2023-09-05 오후 4.02.26.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;2. 다음과 같이 생성해주면 끝&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-05 오후 4.04.47.png&quot; data-origin-width=&quot;2242&quot; data-origin-height=&quot;1500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zo158/btss9z1Fykk/LZyy8dRj6i6RjuefbeLEVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zo158/btss9z1Fykk/LZyy8dRj6i6RjuefbeLEVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zo158/btss9z1Fykk/LZyy8dRj6i6RjuefbeLEVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzo158%2Fbtss9z1Fykk%2FLZyy8dRj6i6RjuefbeLEVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2242&quot; height=&quot;1500&quot; data-filename=&quot;스크린샷 2023-09-05 오후 4.04.47.png&quot; data-origin-width=&quot;2242&quot; data-origin-height=&quot;1500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값으로는 아까 생성한 elastic ip를 주면 된다&lt;/p&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. 이렇게 하고 나면, www.petdori.com으로 트래픽을 보내면 ec2로 가진다.&amp;nbsp;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS/AWS</category>
      <category>AWS</category>
      <category>EC2</category>
      <category>Elastic IP</category>
      <category>Route53</category>
      <category>도메인</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/116</guid>
      <comments>https://jofestudio.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 5 Sep 2023 18:47:37 +0900</pubDate>
    </item>
    <item>
      <title>private subnet에 spring boot, public subnet에 nginx 띄우고 연동하기</title>
      <link>https://jofestudio.tistory.com/115</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;로컬에서 spring boot로 작업중인 서버를 private subnet의 ec2에 띄워보고, public subnet에는 nginx를 띄워서 둘을 연결해본다. 즉 nginx로 보내는 요청이 spring boot로 가게끔 설정해본다. (즉 nginx를 프록시로 활용!)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;private subnet에 띄운 ec2에 스프링부트 서버 배포&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;1. ubuntu베이스의 ec2를 생성한다 (amazon linux 베이스여도 됨). 보안그룹 적용시 public subnet에 둘 nginx로부터 오는 트래픽에 대한 인바운드를 열어야 하며, rds나 elastiCache를 사용하는 경우 그 놈들로 가는 트래픽에 대한 아웃바운드를 열어야 한다. (당연히 nginx쪽에서의 아웃바운드와 rds &amp;amp; elastiCache쪽에서의 인바운드도 열어야 함). 또한 bastion host를 통한 ssh접속을 위해 bastion host로부터 오는 22번 포트 인바운드도 열어둬야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;2. ssh등을 활용해 private subnet에 띄운 ec2에 접속한다. ssh로 접속하는 경우 키가 필요하며, scp(secure copy)를 통해 로컬에 있는 키를 public subnet에 띄운 ec2로 보내서 ssh를 두 번 거치는 방법도 있으나 사실 이는 비밀번호가 걸린 현관문 바로 앞에 열쇠를 두는 행위나 마찬가지다. 따라서 나는 ssh jump라는 방식을 통해 ec2에 접속했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693673470794&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Host somsatang-webserver-1
  HostName 13.124.18.245
  User ubuntu
  IdentityFile /Users/chosanghyun/Downloads/sst.pem

Host somsatang-was-1
  HostName 10.0.1.88
  User ubuntu
  IdentityFile /Users/chosanghyun/Downloads/sst.pem
  ProxyJump somsatang-webserver-1&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;3. 다음 공식문서 링크를 참고해 docker를 설치한다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.docker.com/engine/install/ubuntu/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693670058442&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Install Docker Engine on Ubuntu&quot; data-og-description=&quot;Jumpstart your client-side server applications with Docker Engine on Ubuntu. This guide details prerequisites and multiple methods to install.&quot; data-og-host=&quot;docs.docker.com&quot; data-og-source-url=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; data-og-url=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cruL6P/hyTMe5ghx7/J1yKVP4HWSBLB8RYLLmHA0/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/bYVzaG/hyTL2X2h8g/jp3TbpshnumbQ4f9Z60hRk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.docker.com/engine/install/ubuntu/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cruL6P/hyTMe5ghx7/J1yKVP4HWSBLB8RYLLmHA0/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/bYVzaG/hyTL2X2h8g/jp3TbpshnumbQ4f9Z60hRk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Install Docker Engine on Ubuntu&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jumpstart your client-side server applications with Docker Engine on Ubuntu. This guide details prerequisites and multiple methods to install.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;4. rds나 elastiCache를 사용한다면 application.properties에 적어준 설정에서 엔드포인트들을 적어줘야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1693671568614&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// localhost를 rds 엔드포인트로 바꿔준다. 실험결과 3306 포트는 따로 명시 안해도 되긴 함
spring.datasource.url=jdbc:mysql://localhost:3306/wooyoungsoo

// localhost를 elastiCache redis 엔드포인트로 바꿔준다
spring.data.redis.host=localhost

// 나머지 설정들은 생략했음&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;5. Intellij에서 다음과 같이 우측의 Gradle을 누르고 bootJar을 눌러 jar를 만들어준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-03 오전 1.04.20.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DUOh4/btssSq5fGWI/UzKxXolYvq3quCGtc6CZQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DUOh4/btssSq5fGWI/UzKxXolYvq3quCGtc6CZQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DUOh4/btssSq5fGWI/UzKxXolYvq3quCGtc6CZQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDUOh4%2FbtssSq5fGWI%2FUzKxXolYvq3quCGtc6CZQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;417&quot; height=&quot;348&quot; data-filename=&quot;스크린샷 2023-09-03 오전 1.04.20.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;846&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;6. /build/libs에 jar파일이 생긴다. /build/libs, 즉 jar가 있는 디렉토리로 가서 Dockerfile을 만들어주고 다음과 같이 작성한다&lt;/p&gt;
&lt;pre id=&quot;code_1693670854010&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM openjdk:17
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;openjdk는 프로젝트에서 사용중인 자바 버전에 맞게 11이나 17등을 써주면 된다.&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;FROM openjdk:17: Docker 이미지의 기본 이미지를 지정. 이 경우에는 OpenJDK 17 이미지를 사용하는 것이며, 이 이미지는 Java 17 환경을 제공한다&lt;/li&gt;
&lt;li&gt;ARG JAR_FILE=*.jar: Docker 빌드 시에 사용할 변수를 정의. JAR_FILE이란 변수를 정의한 것이며, 기본값은 현재 디렉토리에 있는 모든 .jar 확장자를 가진 파일 중 하나&lt;/li&gt;
&lt;li&gt;COPY ${JAR_FILE} app.jar: COPY 명령을 사용하여 호스트 머신에서 Docker 컨테이너로 JAR 파일을 복사하라고 하는 것. ${JAR_FILE} 변수는 방금 정의한 그 변수가 맞다. 이 파일은 컨테이너 내부에 app.jar로 복사된다.&lt;/li&gt;
&lt;li&gt;ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]: 컨테이너가 시작될 때 실행할 명령을 정의하는 것. 이 경우에는 Java를 실행하여 app.jar 파일을 실행한다는 의미다. 즉, Docker 컨테이너가 시작되면 Java 애플리케이션을 실행하고 jar 파일(= app.jar)을 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;즉 정리하면&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;&quot;자바 17버전 환경에서 컨테이너를 실행할 건데&quot;,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;&quot;현재 디렉토리에서 .jar파일이 있으면 걜 JAR_FILE이란 변수에 저장하고&quot;,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;&quot;app.jar라는 이름으로 복사한 다음&quot;,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;&quot;컨테이너를 시작할 때 'java -jar'라는 커맨드를 활용해 app.jar를 구동할 거야&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;라는 의미다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;7. 다음 커맨드를 입력해 도커 이미지를 빌드한다&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693671835357&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build {옵션} {도커계정}/{프로젝트명}:{버전} {경로}
ex) docker build -t jofe/toyproject:0.1.0 .&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;-t는 --tag와 같은 의미이며 &lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;이미지에 부여할 이름과 태그를 지정할 수 있게끔 한다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;참고로 맥북 m1이나 m2를 사용중인 유저라면 빌드 플랫폼이 arm기반이 되는데, aws에서 만든 ec2가 amd기반이면 호환성 문제가 생긴다. 따라서 만약 aws에서 만든 ec2가 amd기반이라면 다음과 같이 플랫폼을 지정해 빌드한다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693672055675&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build --platform linux/amd64 {옵션} {도커계정}/{프로젝트명}:{버전} {경로}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;하지만 다음과 같이 ec2를 만들 때 arm기반으로 만들었다면 맥북도 방금 말한 거 안해도 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-03 오전 12.54.45.png&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQiDRM/btssP8KAiR6/gxv1qdFolcx3pHiSQhBpOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQiDRM/btssP8KAiR6/gxv1qdFolcx3pHiSQhBpOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQiDRM/btssP8KAiR6/gxv1qdFolcx3pHiSQhBpOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQiDRM%2FbtssP8KAiR6%2Fgxv1qdFolcx3pHiSQhBpOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;476&quot; height=&quot;352&quot; data-filename=&quot;스크린샷 2023-09-03 오전 12.54.45.png&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;8. 도커허브에 방금 생성한 도커 이미지를 푸시한다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693672233921&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker push {이미지명}
ex) docker push jofe/toyproject:0.1.0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&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;715&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btKPZ1/btssYTLCSoa/cPAH2G2AkHKRkkeVlvyiIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btKPZ1/btssYTLCSoa/cPAH2G2AkHKRkkeVlvyiIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btKPZ1/btssYTLCSoa/cPAH2G2AkHKRkkeVlvyiIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtKPZ1%2FbtssYTLCSoa%2FcPAH2G2AkHKRkkeVlvyiIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;214&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;9. ssh로 접속한 private subnet ec2에서 docker pull로 이미지를 땡겨온다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693672352112&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker pull {이미지명}
ex) sudo docker pull jofe/toyproject:0.1.0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;10. 다음 커맨드를 입력해 컨테이너를 돌린다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693672451498&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run --name {컨테이너명} -d -p 8080:8080 {이미지명}
ex) sudo docker run --name &quot;jofe-service&quot; -d -p 8080:8080 jofe/toyproject:0.1.0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;-p 옵션 : &lt;b&gt;-p 호스트포트:컨테이너포트&lt;/b&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;&lt;b&gt; 형식&lt;/b&gt;으로 사용되며,&amp;nbsp; 이 옵션을 통해 호스트 머신과 컨테이너 간의 포트 포워딩(매핑)을 설정한다. 즉 호스트 머신의 몇 번 포트로 보내면 컨테이너의 몇 번 포트로 보낸다 이런 의미.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;-d 옵션 : 컨테이너를 백그라운드에서 돌리게끔 하는 역할&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;참고 : 호스트의 8080번 포트로 설정한 만큼, 보안그룹에서 nginx 쪽에서 8080포트로 보내는 아웃바운드와 spring boot쪽에서 8080으로 받는 인바운드를 열어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;public subnet에 nginx 띄우고 연결&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;1. ssh로 public subnet에 띄운 ec2에 접속한다. 당연히 외부에서 22번 포트로 오는 ssh접속에 대한 인바운드가 열려있어야 한다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;2. nginx를 설치한다&lt;/p&gt;
&lt;pre id=&quot;code_1693843202256&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;3. nginx를 설정할 차례다. 우선&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; /etc/nginx/sites-available 디렉토리로 이동한다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693843289741&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd /etc/nginx/sites-available&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;4. 이 디렉토리는 nginx의 설정 파일들이 위치했으며, 처음에 들어가면 default라는 기본적인 설정 파일이 있을 것이다. 우리가 사용할 설정파일을 새로 만들어준다. (vi로 해도 상관없음)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693843366017&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo nano {원하는 설정파일명}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;5. 새로 만든 설정파일의 내용은 다음과 같이 설정한다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693843453925&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
        listen 80;
        server_name {nginx가 띄워진 ec2의 ip 주소 또는 도메인};

        location / {
                proxy_pass http://{spring boot를 배포한 서버의 ip 또는 도메인}:8080;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;listen 80 : 80번 포트로 웹서버를 서비스한다는 의미. 이를 통해 &lt;a href=&quot;http://xxx.xxx.xxx.xxx:80이&quot;&gt;http://xxx.xxx.xxx.xxx:80이&lt;/a&gt; 아닌 &lt;a href=&quot;http://xxx.xxx.xxx.xxx로&quot;&gt;http://xxx.xxx.xxx.xxx로&lt;/a&gt; 접속 가능하다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;server_name : 이 값으로는 도메인 or 호스트 or IP주소 작성. server 블록을 여러 개 둘 수 있으며, 클라이언트가 보낸 request의 header안에 있는 Host값이 이 server_name값과 일치하는 server 블록이 활성화된다. 만약 Host값과 매칭되는게 없다면 기본적으로 설정된 server블록 또는 첫 번째로 작성된 server 블록이 활성화된다&lt;/li&gt;
&lt;li&gt;location / {블라블라..} : /에 해당하는 URL, 즉 모든 요청에 대한 설정을 {}안에 정의한다는 뜻 (만약 /api로 했다면 해당 경로로 오는 요청들에 대한 설정을 정의한다는 뜻이 되겠져)&lt;/li&gt;
&lt;li&gt;proxy_pass : /에 해당하는 URL, 즉 모든 요청을 이 URL로 리다이렉트한다&lt;/li&gt;
&lt;li&gt;proxy_set_header : 헤더 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;6. 방금 작성한 설정파일을 nginx가 읽을 수 있도록 설정할 차례.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; /etc/nginx/sites-enabled로 이동한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693844132814&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd /etc/nginx/sites-enabled&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 디렉토리는 /etc/nginx/sites-available에 있는 설정 파일 중 활성화할 설정 파일을 &quot;링크(심볼릭 링크)&quot;로 관리하는 디렉토리다. ls를 누르면 default가 링크돼있는 걸 확인할 수 있고, 다음 커맨드를 통해 default를 지운다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693844385389&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo rm default&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;7. 우리가 아까 만든 설정파일에 링크를 걸어준다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693844451015&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo ln -s /etc/nginx/sites-available/{아까 만든 설정파일명}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심볼릭 링크 : 파일 시스템에서 다른 파일이나 디렉토리를 가리키는 일종의 포인터. 링크된 파일의 내용을 복제하는 개념이 아닌 단순히 참조만 하는, 걍 포인터라고 생각하면 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. nginx를 재시작해 설정 파일을 적용한다&lt;/p&gt;
&lt;pre id=&quot;code_1693844672078&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo service nginx restart&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. nginx가 띄워진 ec2의 ip주소로 요청을 보내면 private subnet에 띄운 spring boot로 전달되는 걸 볼 수 있다&lt;/p&gt;</description>
      <category>PROJECT/개발일지</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/115</guid>
      <comments>https://jofestudio.tistory.com/115#entry115comment</comments>
      <pubDate>Tue, 5 Sep 2023 01:31:47 +0900</pubDate>
    </item>
    <item>
      <title>Redis활용해 Refresh Token 관리하기 (spring boot, elastiCache)</title>
      <link>https://jofestudio.tistory.com/114</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;현재 내가 소마에서 진행 중인 프로젝트는 jwt를 활용중이며, rdb에 refresh token을 저장하고 재발급에 활용하고 있다. 시퀀스 다이어그램으로 보면 다음과 같은 구조다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1280&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TkzMl/btssDJPZld1/izKg8p9mIop9E4HkUwGX51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TkzMl/btssDJPZld1/izKg8p9mIop9E4HkUwGX51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TkzMl/btssDJPZld1/izKg8p9mIop9E4HkUwGX51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTkzMl%2FbtssDJPZld1%2FizKg8p9mIop9E4HkUwGX51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;860&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(참고 : refresh token을 db에 저장한 뒤 클라이언트가 재발급 요청 시 보내진 refresh token과 대조하는 이유 = 악의적 사용자가 지 맘대로 만든 refresh token으로 재발급 요청해서 access token을 받으려 하는걸 막을 수 있고, 악의적 사용자가 refresh token을 탈취했다고 해도 우리 쪽에서 db에 있는 refresh token을 없애준다든가 하는 식으로 조치를 취해줄 수도 있고,, 로그아웃 구현 시에도 편리하고.. 등등)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 이 방법의 단점을 꼽자면 다음과 같은 부분들이 있다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;rdb에서 refresh token을 가져오는데 시간이 좀 걸린다&lt;/li&gt;
&lt;li&gt;rdb에 저장된 토큰데이터들 중 만료기간이 지난 토큰들을 개발자가 코드를 작성하든 뭐 하든 해서 직접 삭제시켜줘야한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이를 인메모리 데이터베이스인 &lt;b&gt;redis&lt;/b&gt;를 사용하면, 해당 단점들을 개선할 수 있다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선 메모리에 저장하기 때문에 rdb대비 토큰을 가져오는 속도가 빠를 것이고&lt;/li&gt;
&lt;li&gt;ttl을 설정해주면 만료기간 지난 토큰들은 알아서 없어진다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 refresh token을 관리하던 방법을 rdb에서 redis로 바꾸기로 했다. 물론 인메모리 데이터베이스인 만큼 꺼지면 다 날라가긴 하지만, refresh token은 날라가도 크리티컬한 피해는 없다. 번거로울 수 있지만 다시 로그인하면 되는 거니까.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;우선 AWS에서 ElastiCache로 Redis를 쓸 거다. 문제는 ElastiCache는 같은 VPC에서만 접속하는걸 허용해서 로컬에서 접근할 수 없다는 것. 그러나 ssh 터널링 등을 통한 방법으로 로컬에서도 ElastiCache에 접근이 가능하다. 이건 내가 쓴 글이 있으니 링크를 달겠다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jofestudio.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jofestudio.tistory.com/110&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693331822819&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;[AWS] SSH 포트포워딩 및 SSM을 통해서 로컬에서 Private Subnet에 있는 RDS 접근하기&quot; data-og-description=&quot;소마 프로젝트를 진행하면서, 인프라 쪽을 내가 어느 정도 담당하고 있어서(사실 담당한다고 하기에도 민망할 정도로 기본적인 부분들만 담당하긴 하지만) RDS를 띄워보기로 했다. Private Subnet에 &quot; data-og-host=&quot;jofestudio.tistory.com&quot; data-og-source-url=&quot;https://jofestudio.tistory.com/110&quot; data-og-url=&quot;https://jofestudio.tistory.com/110&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HLh0q/hyTMg12V10/R4Tmbnj6Sfx12E49oMnAVk/img.png?width=800&amp;amp;height=513&amp;amp;face=0_0_800_513,https://scrap.kakaocdn.net/dn/HjWuv/hyTMgHJL1D/WLppoDkvHRF9Gc310lk8ZK/img.png?width=800&amp;amp;height=513&amp;amp;face=0_0_800_513,https://scrap.kakaocdn.net/dn/AhqZc/hyTL7D2uDX/pxVFjH22lZ8oaM6ZCg3ZP1/img.png?width=1280&amp;amp;height=381&amp;amp;face=0_0_1280_381&quot;&gt;&lt;a href=&quot;https://jofestudio.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jofestudio.tistory.com/110&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HLh0q/hyTMg12V10/R4Tmbnj6Sfx12E49oMnAVk/img.png?width=800&amp;amp;height=513&amp;amp;face=0_0_800_513,https://scrap.kakaocdn.net/dn/HjWuv/hyTMgHJL1D/WLppoDkvHRF9Gc310lk8ZK/img.png?width=800&amp;amp;height=513&amp;amp;face=0_0_800_513,https://scrap.kakaocdn.net/dn/AhqZc/hyTL7D2uDX/pxVFjH22lZ8oaM6ZCg3ZP1/img.png?width=1280&amp;amp;height=381&amp;amp;face=0_0_1280_381');&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;[AWS] SSH 포트포워딩 및 SSM을 통해서 로컬에서 Private Subnet에 있는 RDS 접근하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소마 프로젝트를 진행하면서, 인프라 쪽을 내가 어느 정도 담당하고 있어서(사실 담당한다고 하기에도 민망할 정도로 기본적인 부분들만 담당하긴 하지만) RDS를 띄워보기로 했다. Private Subnet에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jofestudio.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Bastion Host를 통해 접근하는 방식이므로 ElastiCache에 달아주는 보안그룹에는 Bastion Host 쪽에서 오는 6379포트에 대한 인바운드를 열어줘야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;스프링부트에선 다음과 같은 의존성을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1693332034624&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-redis'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 다음 application.properties에 다음과 같이 작성해준다(application.yml을 사용한다면 그에 맞춰 수정하면 됨)&lt;/p&gt;
&lt;pre id=&quot;code_1693332091272&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.data.redis.host=localhost
spring.data.redis.port=6379&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 로컬에서 ssh 포트포워딩을 통해 elastiCache에 접속하는 중이므로 host는 localhost로 한 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 다음과 같은 Configuration Class를 만들어준다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693332191645&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 {
    @Value(&quot;${spring.data.redis.host}&quot;)
    private String host;

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    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());

        // 일반적인 key:value의 경우 시리얼라이저
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;RedisConnectionFactory : Redis 서버와의 연결을 만들고 관리하는 빈&lt;/li&gt;
&lt;li&gt;RedisTemplate : Redis와의 상호작용을 돕는 빈. 즉 얘를 핸들링하여 레디스와 짝짝꿍하는 것&lt;/li&gt;
&lt;li&gt;Serializer설정 : RedisTemplate의 디폴트 Serializer는 JdkSerializationRedisSerializer인데, 문제는 redis-cli를 통해 개발자가 직접 redis안에 있는 애들을 볼 때 Jdk직렬화된 애들은 그 값을 도무지 알아먹을 수가 없어서, 편의를 위해 StringRedisSerializer로 설정(이렇게 하면 String값 그대로 Redis에 저장됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;참고 : 직렬화(seriailize) = 객체 등을 바이트 스트림 형태의 연속적인 데이터로 변환하는 것. 반대는 역직렬화(deserialize)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;그리고 다음과 같은 RefreshTokenService를 만들어준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693640681703&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class RefreshTokenService {
    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
    private final Long REFRESH_TOKEN_EXPIRE_TIME = 60 * 60 * 24 * 30L;

    public void saveRefreshToken(String email, String refreshToken) {
    	// 이메일을 key로, 토큰값을 value로
        redisTemplate.opsForValue().set(email, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
    }

    public String getRefreshToken(String email) {
        Object refreshToken = redisTemplate.opsForValue().get(email);
        return refreshToken == null ? null : (String) refreshToken;
    }

    public void deleteRefreshToken(String email) {
        redisTemplate.delete(email);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;나는 Refresh Token의 유효기간을 1달로 잡았기 때문에, 60 * 60 * 24 * 30 = 30일을 유효기간으로 설정해 저장하도록 구현했다. 분이 아니라 다른 시간 단위로도 저장 가능하니, 본인 취향따라 하면 된다.(TimeUnit.SECONDS 이쪽)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 이를 활용해 원래 rdb를 사용하던 로직을 바꿔줬다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;로그인 후 jwt(access token, refresh token)을 생성하고 refresh token을 rdb에 저장하던 것 =&amp;gt; refresh token을 redis로 저장. 이 때 redis에 기존에 쓰던 refresh token이 있으면 덮어씌운다&lt;/li&gt;
&lt;li&gt;토큰 재발급 요청시 rdb에서 refresh token가져오던 것 =&amp;gt; redis에서 가져온다&lt;/li&gt;
&lt;li&gt;토큰 재발급 후 재발급된 refresh token을 rdb에 저장하던 것 =&amp;gt; redis로 저장. 이 때 redis에 기존에 쓰던 refresh token이 있으면 덮어씌운다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 로그인 요청, 인가인증이 필요한 요청, 토큰 재발급 요청에 대한 수행과정들을 시퀀스 다이어그램으로 표현하면 다음과 같아진다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-11 오후 6.44.02.png&quot; data-origin-width=&quot;3014&quot; data-origin-height=&quot;1932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuCl8E/btstKY66sRq/zXqjAvmTJcgYyl4DpoHNC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuCl8E/btstKY66sRq/zXqjAvmTJcgYyl4DpoHNC0/img.png&quot; data-alt=&quot;ppt로 힘들게 만드러따..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuCl8E/btstKY66sRq/zXqjAvmTJcgYyl4DpoHNC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuCl8E%2FbtstKY66sRq%2FzXqjAvmTJcgYyl4DpoHNC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3014&quot; height=&quot;1932&quot; data-filename=&quot;스크린샷 2023-09-11 오후 6.44.02.png&quot; data-origin-width=&quot;3014&quot; data-origin-height=&quot;1932&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ppt로 힘들게 만드러따..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아 그리고 이 과정에서 추가적으로 고민했던 게, 토큰재발급 요청을 인증된 유저만 받을지였다. 구글링해서 나오는 다른 예제들을 보면 재발급 요청은 인증을 안 해도 가능하게끔 하는 예제가 대부분이었는데, 나는 고민 끝에 재발급 요청도 인증을 한 유저만 할 수 있게끔 했다. 왜냐하면, 당연히 &lt;b&gt;보안을 위해서&lt;/b&gt;. 인증할 필요없이 refresh token만 덜렁 보내서 재발급받을 수 있다면, refresh token이 탈취당하면 너도나도 내 refresh token으로 재발급을 무료로 시도할 수 있기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;인증은 access token을 통해 이뤄지며, 재발급 요청은 우리 플젝에선 앱 접속시 자동로그인을 위한 재발급을 위할 때가 아니라면 access token이 만료됐을 때 이뤄질 거다. 만료된 토큰은 검증과정에서 ExpiredJwtException을 뱉을 텐데, 그러면 만료된 토큰에 대한 인증을 어떻게 할 수 있는가?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;처음에 생각한 것은 임시 통행증(Authentication)을 끊어주는 거였다. 스프링 시큐리티는 ContextHolder에 Authentication이 들어있다면 인증됐다고 판단하므로, ExpiredJwtException이 터졌을 때 재발급경로로 요청이 온 것이라면(이는 request객체를 까서 판단 가능) 임시로 Authentication을 만들어 ContextHolder에 넣어주면 되는거다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693653486231&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
	jwtProvider.validateAccessToken(accessToken);
} catch (ValidTimeExpiredJwtException ex) {
	// access token이 만료됐지만 재발급하는 요청에 대한 처리 - 임시로 인증됐다고 처리한다
	if (request.getRequestURI().equals(REISSUE_PATH)) {
		setTemporaryAuthenticationToContextHolder();
		return;
	}
	request.setAttribute(&quot;exception&quot;, ex.getMessage());
	return;
} catch (CustomJwtException ex) {
	request.setAttribute(&quot;exception&quot;, ex.getMessage());
	return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1693653516129&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private void setTemporaryAuthenticationToContextHolder() {
        // 임시 권한 생성
        List&amp;lt;GrantedAuthority&amp;gt; temporaryAuthorities = new ArrayList&amp;lt;&amp;gt;();
        temporaryAuthorities.add(new SimpleGrantedAuthority(ROLE_USER.name()));
        // 임시 통행증 발급
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                &quot;temporaryAuthentication&quot;, &quot;&quot;, temporaryAuthorities
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 해도 잘 동작한다. 하지만! 나중에 소마 내 다른 연수생이 작성한 코드들을 몰래보다가, ExpiredJwtException의 인스턴스로부터 jwt클레임을 추출 가능하다는 놀라운 사실(!)을 깨달았다. 따라서 단순히 유효기간이 만료된 토큰이라면 정상적인 토큰에서 claim을 빼내는 것처럼 claim을 빼낼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1693653779178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private String extractEmailFromToken(String accessToken) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(accessKey)
                    .build()
                    .parseClaimsJws(accessToken)
                    .getBody()
                    .getSubject();
        } catch (ExpiredJwtException ex) {
            return ex.getClaims().getSubject();
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;역시 남의 코드 보면서 배우는게 참 많아.. 암튼, 이를 통해 ExpiredJwtException이 터지면 재발급 요청인지를 판단하고, 재발급 요청이라면 기존 인증 절차와 마찬가지로 동작시킬 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이런 과정들을 거쳐 redis를 도입했다. 그러면 속도는 얼마나 개선됐을까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-28 오후 8.46.28.png&quot; data-origin-width=&quot;1804&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byHJ8Q/btssUcrAWum/EcnSeiFXqw16k37Vjzrde1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byHJ8Q/btssUcrAWum/EcnSeiFXqw16k37Vjzrde1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byHJ8Q/btssUcrAWum/EcnSeiFXqw16k37Vjzrde1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyHJ8Q%2FbtssUcrAWum%2FEcnSeiFXqw16k37Vjzrde1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1804&quot; height=&quot;500&quot; data-filename=&quot;스크린샷 2023-08-28 오후 8.46.28.png&quot; data-origin-width=&quot;1804&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;RDB(AWS RDS)만 사용했을 땐 평균 180ms정도가 걸렸으나,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-29 오전 3.29.30.png&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkMJv3/btssNlDxbcH/mQKtaB2Yk1Q3XqU9plRb9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkMJv3/btssNlDxbcH/mQKtaB2Yk1Q3XqU9plRb9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkMJv3/btssNlDxbcH/mQKtaB2Yk1Q3XqU9plRb9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkMJv3%2FbtssNlDxbcH%2FmQKtaB2Yk1Q3XqU9plRb9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;816&quot; height=&quot;263&quot; data-filename=&quot;스크린샷 2023-08-29 오전 3.29.30.png&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Redis(AWS ElastiCache)를 사용하고나선 평균 100ms정도가 걸리게 됐다. &lt;b&gt;약 80ms정도&lt;/b&gt;&amp;nbsp;속도가 빨라진 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아직 학생 수준인 내게 80ms 정도라는 숫자가 의미가 있는지는 모르겠다. 그러나 트래픽이 많아질수록, 단축된 시간 X 요청횟수가 실제적인 성능 개선이라고 생각한다면 의미있는 개선을 이뤄낸게 아닐까싶다. okky에 문의한(?) 결과 운영자분께서 elastiCache를 통해 80ms 정도 개선한 것이면 잘 한 것이라고 해서 나름 뿌듯함.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;굳이 레디스를 도입했어야 하는가? 라는 질문도 받을 수 있다. 맞는 말이다. 일단 배포를 안 한 만큼 재발급 요청에 대한 트래픽이 현재 없는 상태인데 굳이 80ms 높이겠다고 레디스? + 사용자 입장에서도 체감되는 개선인가? 라는 질문도 추가적으로 받을 수 있겠다. 한마디로, 지금 레디스를 도입하는 건 오버 엔지니어링이 아니겠냐는 말로 정리할 수 있겠다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 속도 측면 말고도 유효기간이 만료된 토큰이 개발자가 뭘 하지 않아도 레디스에서 날라간다는 점에서 개인적으로 나름의 의미는 있다고 생각한다. 또한 학생 신분인 내가 레디스를 이렇게 처음 접하게 된 것도 의미가 충분한다. 마지막으로 돈 문제. ElastiCache 그거 돈 들지 않느냐! 굳이 레디스 필요없는데 왜 레디스 달아서 돈 나가게 하냐!&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;당연히 실무라면, 혹은 내가 실제로 이 서비스를 운영하고 있는 단계면 어떻게든 돈 적게 쓸라고 쥐어짰겠지만, 난 지금 소마에서 지원금을 받으면서 플젝을 하고 있는 상태. 돈이 좀 드는 AWS 서비스들을 이것저것 써보면서 학습하는게 스스로한테 좋을 터. 그렇기 때문에 Redis를 이렇게 써보는게 의미가 있다고 생각한다. 허허헣&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/개발일지</category>
      <category>JWT</category>
      <category>Redis</category>
      <category>레디스</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/114</guid>
      <comments>https://jofestudio.tistory.com/114#entry114comment</comments>
      <pubDate>Sat, 2 Sep 2023 20:35:57 +0900</pubDate>
    </item>
    <item>
      <title>애플로그인 만들기 - id token(itentity token) 검증하기(java)</title>
      <link>https://jofestudio.tistory.com/113</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;현재 프로젝트를 하면서 소셜로그인 기능을 만드는 중이다. 구글 &amp;amp; 카카오 &amp;amp; 애플 로그인을 만들고 있으며 마지막에 개발하게 된 놈이 바로 애플.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;애플 디벨로퍼 설정은 다음 블로그를 참고했다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dalgoodori.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dalgoodori.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692804786606&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;[Flutter] Apple Login ① - 준비&quot; data-og-description=&quot;마지막으로 구현할 소셜로그인은 애플 로그인 입니다. 애플 로그인을 구현하기 위해서는 개발자 계정에 가입되어있어야하고 비용은 1년에 13만원 입니다. ㅠㅠ Apple Developer There&amp;rsquo;s never been a bette&quot; data-og-host=&quot;dalgoodori.tistory.com&quot; data-og-source-url=&quot;https://dalgoodori.tistory.com/49&quot; data-og-url=&quot;https://dalgoodori.tistory.com/49&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qheoq/hyTIHSLo2Q/Vusk0Z2tekzug1ovgbkqdk/img.png?width=800&amp;amp;height=305&amp;amp;face=0_0_800_305,https://scrap.kakaocdn.net/dn/WFoDL/hyTIH6hoax/nK1vbb3WCW2LAR58gWMbF1/img.png?width=800&amp;amp;height=305&amp;amp;face=0_0_800_305,https://scrap.kakaocdn.net/dn/I2mjU/hyTIG0Dk5G/rDDvNtueLhT8URtMCacZT1/img.png?width=2424&amp;amp;height=930&amp;amp;face=0_0_2424_930&quot;&gt;&lt;a href=&quot;https://dalgoodori.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dalgoodori.tistory.com/49&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qheoq/hyTIHSLo2Q/Vusk0Z2tekzug1ovgbkqdk/img.png?width=800&amp;amp;height=305&amp;amp;face=0_0_800_305,https://scrap.kakaocdn.net/dn/WFoDL/hyTIH6hoax/nK1vbb3WCW2LAR58gWMbF1/img.png?width=800&amp;amp;height=305&amp;amp;face=0_0_800_305,https://scrap.kakaocdn.net/dn/I2mjU/hyTIG0Dk5G/rDDvNtueLhT8URtMCacZT1/img.png?width=2424&amp;amp;height=930&amp;amp;face=0_0_2424_930');&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;[Flutter] Apple Login ① - 준비&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 구현할 소셜로그인은 애플 로그인 입니다. 애플 로그인을 구현하기 위해서는 개발자 계정에 가입되어있어야하고 비용은 1년에 13만원 입니다. ㅠㅠ Apple Developer There&amp;rsquo;s never been a bette&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dalgoodori.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 플러터에서 애플로그인하는 기능은 다음 패키지를 활용했다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/sign_in_with_apple&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://pub.dev/packages/sign_in_with_apple&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692804844389&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;sign_in_with_apple | Flutter Package&quot; data-og-description=&quot;Flutter bridge to initiate Sign in with Apple (on iOS, macOS, and Android). Includes support for keychain entries as well as signing in with an Apple ID.&quot; data-og-host=&quot;pub.dev&quot; data-og-source-url=&quot;https://pub.dev/packages/sign_in_with_apple&quot; data-og-url=&quot;https://pub.dev/packages/sign_in_with_apple&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cnHajL/hyTIHFc0Zf/DO1rH2QtLVaLHjjJFV82NK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/nQi13/hyTINenJGU/iNfksHjAA4PPKyhJnsLkzK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/cOepwX/hyTIPXzWEh/gNu3YTrHgTEFklDdbSxdi0/img.png?width=924&amp;amp;height=925&amp;amp;face=0_0_924_925&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/sign_in_with_apple&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pub.dev/packages/sign_in_with_apple&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cnHajL/hyTIHFc0Zf/DO1rH2QtLVaLHjjJFV82NK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/nQi13/hyTINenJGU/iNfksHjAA4PPKyhJnsLkzK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/cOepwX/hyTIPXzWEh/gNu3YTrHgTEFklDdbSxdi0/img.png?width=924&amp;amp;height=925&amp;amp;face=0_0_924_925');&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;sign_in_with_apple | Flutter Package&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Flutter bridge to initiate Sign in with Apple (on iOS, macOS, and Android). Includes support for keychain entries as well as signing in with an Apple ID.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pub.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;설정방법도 꽤나 상세히 나와있으니, 플러터로 애플로그인 구현하시는 분들은 저 2개 링크타고 가서 보면 될듯&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;암튼 뭐 그렇게 해서 플러터에서 애플로그인하는 기능은 만들었다. 구글 &amp;amp; 카카오 로그인 만들 때는 access token을 바로 플러터 단에서 얻게 되는 방식이었는데, 애플은 access token이 아니라 &lt;b&gt;authorization code&lt;/b&gt;랑 &lt;b&gt;identity token&lt;/b&gt;이란 걸 준다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;나는 현재 프론트 단에서 access token을 받아서 그걸 백엔드로 넘긴 뒤 백엔드에서 해당 토큰을 통해 유저 정보를 조회하는 방식으로 만들고 있었기 때문에, 발급받은 identity token으로 사용자 정보를 조회할 수 있는 엔드포인트가 있나를 먼저 살폈다(구글, 카카오는 access token 통해서 사용자 정보 조회하는 엔드포인트가 있기 때문..) 그러나! &lt;b&gt;애플은 그런 엔드포인트가 없었다&lt;/b&gt;.. 애플 개발자문서에도 지들은 그런 엔드포인트 없다고 말하고 있음. ㅋㅋ&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 대신에 처음에 로그인 성공했을 때 &lt;b&gt;authorization code랑 identity token말고도 로그인한 유저의 정보(이메일 등)을 주긴 준다.&lt;/b&gt; 이 정보를 백엔드로 보낸다면? 이란 생각이 들었으나, 바로 철회했다. 지금 플젝에서 어차피 보내게 될 정보야 끽해야 이메일인데, 사실상 이메일과 플랫폼명(애플, 카카오 등)을 받아서 로그인해주는 api를 만드는 건 보안상 매우 위험하기 때문. 공격자들이 내 이메일을 안다면, 지들도 로그인하는 엔드포인트로 내 이메일이랑 플랫폼명만 보내면 로그인된다면 그 서비스를 누가 이용하겠는가. 사실 구글, 카카오 로그인 만들 때도 같은 이유로 프론트 단에서 회원정보가 아니라 플랫폼들로부터 받은 access token을 넘기게 했던 거고.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 어떻게 해야 할까? &lt;b&gt;애플로부터 발급받은 identity token에는 공급자, 대상자, 유효시간, 사용자 이메일과 같은 정보들도 들어있다&lt;/b&gt;. 따라서 identity token을 백엔드로 보내고 백엔드에서 사용자 이메일을 꺼내서 사용해야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;전달해주는건 쉽지. 그러면 어떻게 꺼내야 하는가? &lt;b&gt;identity token은 일단 jwt형식&lt;/b&gt;이어서, 이 토큰을 만들 때 사용했던 키를 몰라도 디코딩해서 내용을 볼 수 있다. 애플이 지들 키 써서 만든 토큰인데 당연히 내가 그 키를 알 방법은 없고, 직접 디코딩해서 보는 식으로 해야겠군! 이라는 생각이 들었다. split메서드와 Base64 decoder를 이용해 디코딩해서 금방 email값을 뽑아낼 수 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 이 방법, 생각해보니 &lt;b&gt;보안적으로 위험하다&lt;/b&gt;. 우선 구글과 카카오에서 썼던 &quot;토큰을 동봉해 유저정보를 조회하는 엔드포인트로 요청을 보내서 받는 방식&quot;은 내부적으로 그 토큰이 유효한 토큰인지 검증해줄 것이다. 근데 지금 내가 애플에서 온 토큰을 직접 까는 이 방식은 요 토큰이 유효한 토큰인지, 즉 제대로 된 토큰인지 검증하지 않는다. 한마디로 악의적 사용자가 대충 내 이메일을 알아낸다음 내 이메일을 jwt로 만들어서 우리 백엔드의 애플로그인 엔드포인트로 요청을 보내면 로그인이 되는 말도 안 되는 상황이 연출될 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;한마디로, 넘겨받은 &lt;b&gt;identity token이 합법적인 토큰인지 검증할 필요가 있다&lt;/b&gt;. 찾아보니 공식 문서에 해당 내용에 대한 글이 있었다. 다행쓰..&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;애플은 다음과 같이 5가지 스텝을 통해 identity token을 우리가 알아서(?) 검증하라고 한다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;990&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc7PWq/btsr6YNHkio/LpIGkXKXkaw7oGmwdqKnuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc7PWq/btsr6YNHkio/LpIGkXKXkaw7oGmwdqKnuK/img.png&quot; data-alt=&quot;https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc7PWq/btsr6YNHkio/LpIGkXKXkaw7oGmwdqKnuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc7PWq%2Fbtsr6YNHkio%2FLpIGkXKXkaw7oGmwdqKnuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;990&quot; height=&quot;502&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;애플의 공개키로 identity token의 서명을 확인하라는 내용을 볼 수 있다. 아니 근데 애플 양반 당신들이 쓰는 키를 내가 어떻게 알아!라고 말하고 싶지만 보다시피 &lt;b&gt;공개키&lt;/b&gt;다 공개키.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;애플은 다음 링크에 있는 엔드포인트를 통해 자기들이 현재 시점에서 사용중인 키들의 정보를 제공한다. 허허허..&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692806606325&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;Fetch Apple&amp;rsquo;s public key for verifying token signature | Apple Developer Documentation&quot; data-og-description=&quot;Fetch Apple&amp;rsquo;s public key to verify the ID token signature.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biOPa8/hyTIDW84Lg/5CQ1EGKjEL45PJ5niGhkKk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/3nmlV/hyTICcRsnF/qQJURVTuErqCNm6uL56CZK/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biOPa8/hyTIDW84Lg/5CQ1EGKjEL45PJ5niGhkKk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/3nmlV/hyTICcRsnF/qQJURVTuErqCNm6uL56CZK/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&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;Fetch Apple&amp;rsquo;s public key for verifying token signature | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Fetch Apple&amp;rsquo;s public key to verify the ID token signature.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 형식일 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692806731409&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;keys&quot;: [
    {
      &quot;kty&quot;: &quot;RSA&quot;,
      &quot;kid&quot;: &quot;YuyXoY&quot;,
      &quot;use&quot;: &quot;sig&quot;,
      &quot;alg&quot;: &quot;RS256&quot;,
      &quot;n&quot;: &quot;1JiU4l3YCeT4o0gVmxGTEK1IXR-Ghdg5Bzka12tzmtdCxU00ChH66aV-4HRBjF1t95IsaeHeDFRgmF0lJbTDTqa6_VZo2hc0zTiUAsGLacN6slePvDcR1IMucQGtPP5tGhIbU-HKabsKOFdD4VQ5PCXifjpN9R-1qOR571BxCAl4u1kUUIePAAJcBcqGRFSI_I1j_jbN3gflK_8ZNmgnPrXA0kZXzj1I7ZHgekGbZoxmDrzYm2zmja1MsE5A_JX7itBYnlR41LOtvLRCNtw7K3EFlbfB6hkPL-Swk5XNGbWZdTROmaTNzJhV-lWT0gGm6V1qWAK2qOZoIDa_3Ud0Gw&quot;,
      &quot;e&quot;: &quot;AQAB&quot;
    },
    {
      &quot;kty&quot;: &quot;RSA&quot;,
      &quot;kid&quot;: &quot;fh6Bs8C&quot;,
      &quot;use&quot;: &quot;sig&quot;,
      &quot;alg&quot;: &quot;RS256&quot;,
      &quot;n&quot;: &quot;u704gotMSZc6CSSVNCZ1d0S9dZKwO2BVzfdTKYz8wSNm7R_KIufOQf3ru7Pph1FjW6gQ8zgvhnv4IebkGWsZJlodduTC7c0sRb5PZpEyM6PtO8FPHowaracJJsK1f6_rSLstLdWbSDXeSq7vBvDu3Q31RaoV_0YlEzQwPsbCvD45oVy5Vo5oBePUm4cqi6T3cZ-10gr9QJCVwvx7KiQsttp0kUkHM94PlxbG_HAWlEZjvAlxfEDc-_xZQwC6fVjfazs3j1b2DZWsGmBRdx1snO75nM7hpyRRQB4jVejW9TuZDtPtsNadXTr9I5NjxPdIYMORj9XKEh44Z73yfv0gtw&quot;,
      &quot;e&quot;: &quot;AQAB&quot;
    },
    {
      &quot;kty&quot;: &quot;RSA&quot;,
      &quot;kid&quot;: &quot;W6WcOKB&quot;,
      &quot;use&quot;: &quot;sig&quot;,
      &quot;alg&quot;: &quot;RS256&quot;,
      &quot;n&quot;: &quot;2Zc5d0-zkZ5AKmtYTvxHc3vRc41YfbklflxG9SWsg5qXUxvfgpktGAcxXLFAd9Uglzow9ezvmTGce5d3DhAYKwHAEPT9hbaMDj7DfmEwuNO8UahfnBkBXsCoUaL3QITF5_DAPsZroTqs7tkQQZ7qPkQXCSu2aosgOJmaoKQgwcOdjD0D49ne2B_dkxBcNCcJT9pTSWJ8NfGycjWAQsvC8CGstH8oKwhC5raDcc2IGXMOQC7Qr75d6J5Q24CePHj_JD7zjbwYy9KNH8wyr829eO_G4OEUW50FAN6HKtvjhJIguMl_1BLZ93z2KJyxExiNTZBUBQbbgCNBfzTv7JrxMw&quot;,
      &quot;e&quot;: &quot;AQAB&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;즉 정상적인 경우라면, 내가 아까 발급받은 identity token은 위에 있는 키 후보들(?) 중 하나로 만들어진 셈이다. 즉 위 후보들 중 실제로 내가 받은 identity token을 만들 때 써진 키를 찾아야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 어떻게 찾느냐? identity token은 아까 말했듯 jwt다. jwt의 header 부분에는 어떤 알고리즘을 썼는지에 대한 정보들이 있는데, identity token도 그와 마찬가지로 헤더에 관련 정보들이 있다. 구체적으로는 alg값과 kid값이 들어있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 위에 있는 후보 키들을 보면, 각각 kid값과 alg값을 갖고 있는 걸 볼 수 있다. &lt;b&gt;즉, identity token의 header에 들어있는 alg &amp;amp; kid와 동일한 값을 갖는 후보 키가 identity token을 만들 때 쓰인 키&lt;/b&gt;다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;말은 쉽지~ 코드를 보여줘! 난 다음과 같이 구현했다. 전체 코드가 아닌 메서드들만..허헣&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692807186856&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // identity token의 헤더에서 alg, kid 추출해서 저장하는 메서드
    private Map&amp;lt;String, String&amp;gt; getAlgAndKidFromIdToken(String idToken) throws ParseException {
        Map&amp;lt;String, String&amp;gt; algAndKid = new HashMap&amp;lt;&amp;gt;();

        String header = idToken.split(&quot;\\.&quot;)[0];
        Base64.Decoder decoder = Base64.getUrlDecoder();
        JSONObject headerContent = (JSONObject) jsonParser.parse(
                new String(decoder.decode(header))
        );

        algAndKid.put(&quot;alg&quot;, (String) headerContent.get(&quot;alg&quot;));
        algAndKid.put(&quot;kid&quot;, (String) headerContent.get(&quot;kid&quot;));

        return algAndKid;
    }

    // 애플의 공개키들을 받는 엔드포인트로부터 키 후보들을 가져오는 메서드
    private JSONArray getAvailablePublicKeyObjects() throws ParseException {
        HttpEntity&amp;lt;String&amp;gt; httpEntity = new HttpEntity&amp;lt;&amp;gt;(new HttpHeaders());
        ResponseEntity&amp;lt;String&amp;gt; res = restTemplate.exchange(
                APPLE_PUBLIC_KEY_URL, HttpMethod.GET, httpEntity, String.class);
        JSONObject availablePublicKeysContent = (JSONObject) jsonParser.parse(res.getBody());
        return (JSONArray) availablePublicKeysContent.get(&quot;keys&quot;);
    }

    // 키 후보들과 identity token에서 뽑아낸 alg, kid를 비교해 매칭되는 키를 찾는 메서드
    private JSONObject findMatchedPublicKeyObj(JSONArray availablePublicKeyObjects, String alg, String kid) {
        if (availablePublicKeyObjects == null || availablePublicKeyObjects.size() == 0) {
            throw new AppleKeyInfoNotReceivedException();
        }

        for (JSONObject keyObj : (Iterable&amp;lt;JSONObject&amp;gt;) availablePublicKeyObjects) {
            String algFromKey = (String) keyObj.get(&quot;alg&quot;);
            String kidFromKey = (String) keyObj.get(&quot;kid&quot;);

            if (Objects.equals(algFromKey, alg) &amp;amp;&amp;amp; Objects.equals(kidFromKey, kid)) {
                return keyObj;
            }
        }

        return null;
    }
    
    // 찾아낸 후보 키(jsonObject)를 공개키 인스턴스로 만드는 메서드
    private PublicKey generatePublicKey(JSONObject applePublicKeyObj) {
        if (applePublicKeyObj == null) {
            throw new MatchedKeyNotFoundException();
        }

        String kty = (String) applePublicKeyObj.get(&quot;kty&quot;);
        byte[] modulusBytes = Base64.getUrlDecoder().decode((String) applePublicKeyObj.get(&quot;n&quot;));
        byte[] exponentBytes = Base64.getUrlDecoder().decode((String) applePublicKeyObj.get(&quot;e&quot;));

        RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(
                new BigInteger(1, modulusBytes),
                new BigInteger(1, exponentBytes)
        );

        try {
            KeyFactory keyFactory = KeyFactory.getInstance(kty);
             return keyFactory.generatePublic(publicKeySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new ApplePublicKeyNotGenerateException();
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 만든 키를 활용해, identity token의 서명을 확인할 수 있을 것이다. 한 마디로 이 키로 합법적으로 토큰을 깔 때(?) 오류가 안 나야 한다는 말.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 뒤에 nonce검증이 있는데, 이건 패쓰.. nonce값이 토큰에 안 담겨있어서,,허허&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;iss, aud 검증은 다음과 같이 했다. 단순히 equal 비교&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692807612886&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public void verifyAppleIdTokenClaim(Claims claims) {
        if (!claims.getIssuer().equals(issuer)) {
            throw new IssuerNotMatchedException();
        }

        if (!claims.getAudience().equals(clientId)) {
            throw new ClientIdNotMatchedException();
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;유효기간 만료의 경우 이미 키를 통해 claims를 얻는 과정이 잘 통과되면 검증된 것과 마찬가지여서 굳이 넣지는 않았다. 필요하다면 claims의 getExpiration과 before같은 걸 조합해서 만들 수 있을 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/개발일지</category>
      <category>id token</category>
      <category>identity token</category>
      <category>id토큰</category>
      <category>java</category>
      <category>Verification</category>
      <category>검증</category>
      <category>애플로그인</category>
      <category>플러터</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/113</guid>
      <comments>https://jofestudio.tistory.com/113#entry113comment</comments>
      <pubDate>Thu, 24 Aug 2023 01:22:02 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] 스프링 시큐리티의 인증/인가 과정(6.1.2버전 기준, with 공식문서)</title>
      <link>https://jofestudio.tistory.com/112</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;읽기 전에 : 해당 글은 스프링 시큐리티 6.1.2버전 기준으로, 읽는 시점에 따라 다른(deprecate됐다든지) 내용일 수도&lt;/b&gt; &lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #ee2323;&quot;&gt;&lt;i&gt;&lt;b&gt;있습니다.&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;어떠한 요청을 보냈을 때, 요청을 보낸 이에 대한 인가/인증이 수반돼야 한다. 이때&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증(Authentication) : 자원에 접근하는 사용자의 신원을 검증하는 것(너 누군데?)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인가(Authorization) : 사용자가 그 자원에 대해 접근할 수 있는지 확인하는 것(누군지 알겠는데, 너가 그런 권한이 있어?)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇다면 스프링 진영에서 보안을 담당하는 대표적인 스프링 하위 프레임워크인 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;스프링 시큐리티&lt;/b&gt;&lt;/span&gt;에서는 인증과 인가를 어떤 방식으로 해줄까? 이에 대해 알아보자&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그 전에 알고 가면 좋은 지식&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;SecurityContextHolder&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;486&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mKmfN/btsrBZOvanx/kwATs7rOtBVQSsK8sqIEo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mKmfN/btsrBZOvanx/kwATs7rOtBVQSsK8sqIEo0/img.png&quot; data-alt=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-securitycontextholder&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mKmfN/btsrBZOvanx/kwATs7rOtBVQSsK8sqIEo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmKmfN%2FbtsrBZOvanx%2FkwATs7rOtBVQSsK8sqIEo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;486&quot; height=&quot;179&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-securitycontextholder&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스프링 시큐리티가 인증된 사용자의 정보(Authentication)을 SecurityContext에 저장하는데 이 놈을 가지고 있는 바구니가 바로 SecurityContextHolder다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;한 마디로 여기에 사용자의 정보(Authentication)이 있으면 인증된 것으로 취급&lt;/b&gt;&lt;/span&gt;하며, 나중에 이렇게 담은 정보(Authentication)을 통해 인가를 한다. 참고로 어떻게 넣는지는 상관없이 걍 여기에 정보(Authentication)가 들어있기만 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Authentication&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring Security에서 발급해주는 통행증이라고 생각하면 된다. 이 통행증은 다음과 같은 내용들을 가진다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;Principal : 보안 주체에 해당하는 개념으로, 쉽게 말해 &quot;누구냐&quot;에 해당. 보통 UserDetails라는 &quot;사용자의 정보가 담긴 문서&quot;가 이 역할을 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Credential : 주로 비밀번호&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Authorities : Principal이 뭘 할 수 있는지에 대한 권한, 즉 Principal에게 부여된 권한들이며 GrantedAuthority들이 이 역할을 한다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인증&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 사용자를 인증하는 일반적인 방식은 사용자로부터 하여금 이름과 비밀번호를 입력하도록 요구하는 것이다. 이를 통해 인증이 수행되면 ID(세션 ID)를 발급하고, 이를 통한 인증을 수행할 수 있다. 스프링 시큐리티 역시 이름과 비밀번호를 통한 인증 방식을 지원하며, 다음과 같은 과정을 거쳐 진행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;784&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5FFI0/btsrCUlC61w/xbdPzAcDQsZThvwd8WdkF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5FFI0/btsrCUlC61w/xbdPzAcDQsZThvwd8WdkF1/img.png&quot; data-alt=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5FFI0/btsrCUlC61w/xbdPzAcDQsZThvwd8WdkF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5FFI0%2FbtsrCUlC61w%2FxbdPzAcDQsZThvwd8WdkF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;511&quot; height=&quot;386&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;BasicAuthenticationFilter(폼로그인의 경우, UsernamePasswordAuthenticationFilter가 이 역할을 함)가 클라이언트로부터 온 HttpServletRequest로부터 username과 password를 뽑아낸다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;뽑아낸 내용을 바탕으로 UsernamePasswordAuthenticationToken(Authentication의 일종)이란 걸 만든다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자를 인증하기 위해 이 토큰을 AuthenticationManager란 놈에게 보낸다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Authenticatoin이 이 토큰을 바탕으로 &quot;인증&quot;을 시도한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증에 실패하면 -&amp;gt; SecurityContextHolder가 비워지고, RememberMeServices의 loginFail이 호출되며, AuthenticationEntryPoint가 호출된다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증에 성공하면 -&amp;gt; 해당 인증 정보가 SecurityContextHolder에 설정 즉 저장되고, RememberMeServices의 loginSuccess가 호출되고 filterChain.doFilter를 호출해 남은 로직(다음 필터들에서 진행될 것들)을 수행한다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 생각해보면.. 매번 요청을 보낼 때마다 이름과 비밀번호를 함께 보내는 방식으로 인증받는 건 당연히 번거롭다. 최초 로그인할 때만 이렇게 하고 SecurityContext를 세션(HttpSession)에 저장한뒤 세션 id를 클라이언트에게 발급한다. 이후 다시 접속하면 세션으로부터 SecurityContext를 가져와 SecurityContextHolder에 꽂아준다. 따라서 해당 SecurityContext가 Authentication을 갖고 있으면 인증상태가 유지된다. (앞서 말했듯, SecurityContext에 Authentication이 들어있어야 인증된 상태라고 취급하기 때문이다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 내가 의문이 든 것은, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;로그인 이후의 요청에 대해 왜 다시 SecurityContext를 다시 SecurityContextHolder에 꽂아주는가&lt;/b&gt;&lt;/span&gt;였다. 이미 로그인할 때 Authentication이 들어있는 SecurityContext를 SecurityContextHolder에 넣어줬을텐데?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 이유는 간단했다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;사용자가 보낸 request를 처리하는 스레드가 종료되면&lt;/b&gt;&lt;b&gt;&amp;nbsp;SecurityContextHolder를 비워주기 때문&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이다. 그래서 다시 세션으로부터 SecurityContext를 가져와 SecurityContextHolder에 꽂아주는거다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드레벨에서 보면 쉽게 파악할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692606569014&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 간단히 말하면 필터체인의 앞단에서 SecurityContext를 로드하는 걸 담당하는 필터
public class SecurityContextHolderFilter extends GenericFilterBean {
	// ..생략
	private final SecurityContextRepository securityContextRepository;
	// ..생략

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
		// 대충 처음 접속하면 이 if조건문의 doFilter가 실행됨
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		// SecurityContext를 가져오는 모습
		Supplier&amp;lt;SecurityContext&amp;gt; deferredContext = this.securityContextRepository.loadDeferredContext(request);
		try {
			this.securityContextHolderStrategy.setDeferredContext(deferredContext);
			chain.doFilter(request, response);
		}
		finally {
			// 마지막에 SecurityContextFilter를 비워내는 모습!
			this.securityContextHolderStrategy.clearContext();
			request.removeAttribute(FILTER_APPLIED);
		}
	}
    
	// ..생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음과 같이 Session에서 SecurityContext를 가져오는 모습도 코드레벨에서 훔쳐볼 수 있다&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692606880861&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
	// ..생략

	@Override
	public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
		// 세션으로부터 SecurityContext를 가져오는 모습
		Supplier&amp;lt;SecurityContext&amp;gt; supplier = () -&amp;gt; readSecurityContextFromSession(request.getSession(false));
		return new SupplierDeferredSecurityContext(supplier, this.securityContextHolderStrategy);
	}
    
	// ..생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;SecurityContextHolder는 ThreadLocal을 사용&lt;/b&gt;&lt;/span&gt;한다. 즉 SecurityContextHolder는 여러 쓰레드가 공유하는 요소가 아니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692605136169&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SecurityContextHolder의 ThreadLocalSecurityContextHolderStrategy 코드 일부
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static final ThreadLocal&amp;lt;Supplier&amp;lt;SecurityContext&amp;gt;&amp;gt; contextHolder = new ThreadLocal&amp;lt;&amp;gt;();

	@Override
	public void clearContext() {
		contextHolder.remove();
	}

	@Override
	public SecurityContext getContext() {
		return getDeferredContext().get();
	}
    
    // ... 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;보다시피 ThreadLocal을 내부적으로 사용하는 모습을 볼 수 있다. &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;각 쓰레드별로 서로 다른 내용물을 갖는 SecurityContextHolder를 쓰게 되는 것이며, 이를 통해 사용자별로 SecurityContextHolder내에 서로 다른 인증객체(Authentication)을 가지게 되는 효과를 준다! 동일한 쓰레드 내에선 언제든지 SecurityContext를 참조가능하다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;ThreadLocal : 쓰레드마다 할당되는 자신만의 고유한 공간. 멀티쓰레드 환경이어도 ThreadLocal은 다른 쓰레드로부터 안전한 공간이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;즉 정리하자면&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자별로 보내는 요청들을 쓰레드를 통해 처리한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;username, password기반으로 이런 요청들에 대한 인증 작업을 한다. 인증이 완료되면 SecurityContext에 Authentication을 꽂아넣으며, Spring Security는 SecurityContext에 Authentication이 들어가있는지 여부를 통해 인증됐는지 아닌지를 판단한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 매번 username, password를 함께 보낼 순 없으니 세션을 활용한다. 세션id를 통해 세션으로부터 SecurityContext를 가져와 이를 SecurityContextHolder에 꽂아넣는 식이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자별 Authentication 구분은 SecurityContextHolder가 ThreadLocal을 사용함으로써 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;인가&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;인증 과정이 끝났다면, SecurityContextHolder에 SecurityContext가 들어있고 이 안에 Authentication이란 &quot;통행증&quot;이 들어있으며 이를 통해 인가를 진행할 수 있다. Authentication은 내부적으로 Principal(보안 주체에 해당하는 개념)의 authorities(권한)들을 가지며, 구체적으론 GrantedAuthority인터페이스의 구현체들이 Collection형태로 들어가 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;GrantedAuthority의 getAuthority()메서드를 통해 권한 정보들을 읽을 수 있으며, AccessDicisionManager라는 놈이 이 권한 정보들을 읽어서 접근 가능한지 아닌지를 판단해준다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;인가는 다음 과정을 통해 진행된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1280&quot; data-origin-height=&quot;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZLCXP/btsrRYVkjrC/P4HW8DnRxjobgDAX79S0U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZLCXP/btsrRYVkjrC/P4HW8DnRxjobgDAX79S0U0/img.png&quot; data-alt=&quot;https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZLCXP/btsrRYVkjrC/P4HW8DnRxjobgDAX79S0U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZLCXP%2FbtsrRYVkjrC%2FP4HW8DnRxjobgDAX79S0U0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;687&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;필터체인의 AuthorizationFilter가 SecurityContextFilter로부터 Authentication(통행증)을 받아온다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;만약 Authentication을 받아오는데 실패하면 AuthenticationException을 던진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AuthorizationManager(&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;얘가 &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;AccessDicisionManager역할도 해준다&lt;/b&gt;&lt;/span&gt;)&lt;/span&gt;에게 받아온 Authentication과 HttpServletRequest를 전달한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AuthorizationManager가 해당 request를 등록된 패턴과 match되는지를 판단해준다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;거부됐다면 -&amp;gt; AuthorizationDeniedEvent가 발행되고 AccessDeniedException을 던진다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;승인됐다면 -&amp;gt; AuthorizationGrantedEvent가 발행되고 필터체인을 마저 진행한다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;AuthorizationFilter에서 Authentication을 받아오는 메서드의 코드는 다음과 같으며, Authentication을 못 받아오면 예외를 던지는 모습을 직관적으로 바로 확인가능하다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692618310856&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Authentication getAuthentication() {
	Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
	if (authentication == null) {
		throw new AuthenticationCredentialsNotFoundException(
			&quot;An Authentication object was not found in the SecurityContext&quot;);
	}
	return authentication;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;참고로 AuthenticationCredentialsNotFoundException은 AuthenticationException을 상속받은 클래스다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;또한 디폴트 설정은 필터체인의 마지막 필터가 AuthorizationFilter이니 알아두면 좋다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;인증/인가 실패 시에 대한 예외처리&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;인증에 실패하면 AuthenticationException이, 인가에 실패하면 AccessDeniedException이 던져진다. 이들은 어떻게 처리되는가?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;바로 필터체인의 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ExceptionTranslationFilter&lt;/b&gt;&lt;/span&gt;에서 처리된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UTvwV/btsrIaPFsQJ/OnDWgkMX2RNjC6yMpFCp10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UTvwV/btsrIaPFsQJ/OnDWgkMX2RNjC6yMpFCp10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UTvwV/btsrIaPFsQJ/OnDWgkMX2RNjC6yMpFCp10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUTvwV%2FbtsrIaPFsQJ%2FOnDWgkMX2RNjC6yMpFCp10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;698&quot; height=&quot;610&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;이 필터에서 doFilter를 통해 필터체인의 로직들을 수행하다가, AuthenticationException이나 AccessDeniedException이 발생하면 그에 대한 처리를 해주는거다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;유저가 인증되지 않았거나 AuthenticationException이 발생하면&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SecurityContextHolder를 비움&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증 성공 후 다시 replay가 가능하게끔 클라이언트에게 온 request를 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AuthenticationEntryPoint라는 놈을 이용해 클라이언트에게 자격 증명을 요청(즉 클라이언트에게 너 누군지 신원을 대라고 하는 거다. 여기선 AuthenticationEntryPoint가 인증 실패시 써먹는 놈이라고만 알고 넘어가면 된다)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;AccessDeniedException이 발생하면, 심플하다. 등록된 AccessDeniedHandler를 호출한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;이를 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;수도 코드&lt;/b&gt;&lt;/span&gt;로 보면 다음과 같다고 보면 된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692618201462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		// 클라이언트에게 자격 증명을 요청(인증 실패했으니까)
		startAuthentication();
	} else {
		// AccessDeniedHandler 호출
		accessDenied();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;실제로 ExceptionTranslationFilter 내부를 뜯어보면 다음과 같은 코드를 볼 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692619156715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
	FilterChain chain, RuntimeException exception) throws IOException, ServletException {
	if (exception instanceof AuthenticationException) {
		handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
	}
	else if (exception instanceof AccessDeniedException) {
		handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;handleAuthenticationException메서드는 내부적으로 sendStartAuthentication메서드를 호출하며, 코드는 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692619337404&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
	AuthenticationException reason) throws ServletException, IOException {
	// SEC-112: Clear the SecurityContextHolder's Authentication, as the
	// existing Authentication is no longer considered valid
	SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
	this.securityContextHolderStrategy.setContext(context);
	this.requestCache.saveRequest(request, response);
	this.authenticationEntryPoint.commence(request, response, reason);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;request를 저장하고, AuthenticationEntryPoint의 commence라는 메서드를 호출하는 모습을 볼 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;handleAccessDiniedException메서드는 AccessDiniedHandler의 handle메서드를 호출한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;나중에 본인이 커스텀한 AuthenticationEntryPoint와 AccessDeniedHandler를 이용해, 인증/인가를 실패했을 때에 대한 처리방식을 직접 지정해줄 수도 있어서 이렇게 설명해봤다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스프링 시큐리티 원리에 대한 내용을 계속 구글링하면서 뒤져봤는데, 구글링해서 보는 페이지마다 다 같은 내용들만 써져 있어서 내가 궁금한 내용들에 대한 서치가 힘들었다. 각 용어들에 대한 개념은 블로그들을 보면서 어느 정도 알겠는데, 어떤 과정으로 진행되는지에 대한 궁금증들이 있었다. 그러던 중 그냥 공식 문서를 파보자는 생각으로 공식 문서들을 파봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 이전에 React 공부할 때를 빼면, 공식문서를 이렇게까지(물론 지금도 엄청나게 한 것도 아니지만) 공부한 적은 없었다. 개발자로 산다는 사람이 공식문서를 진득하게 본 적 없다는게 사실 생각해보면 부끄러운 일인 것 같다. 처음부터 공식문서를 보는건 개념 잡기가 어렵다는 생각이 들어 지금껏 새로운 걸 공부할 때마다 공식문서로 시작하는 걸 기피해왔던 것 같기도.. 근데 이번에 느낀게, 구글링해서 블로그들 보면서 어느 정도 용어들에 대한 개념만 잡고 그 후엔 공식문서로 공부하는게 되게 좋은 방법 같다. 오피셜한 문서인 만큼 내용에 대한 의심이 안 들고, 정리가 잘 돼있다. 지금 다룬 스프링 시큐리티만 해도 글을 작성하는 현재를 기준으로 구글링해서 나오는 블로그들은 다 옛날 버전이라 deprecate된 내용들이 많다. 예를 들면 인가 과정에서 FilterSecurityInterceptor를 쓴다고들 하는데 6.1.2버전 기준 FilterSecurityInterceptor는 deprecate됐다. 또 SecurityContextPersistenceFilter도 deprecate됐다. 이들 대신에 AuthorizationFilter와 SecurityContextHolderFilter를 현재는 사용중이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;암튼 앞으로 내가 모르는 기술들을 공부할 땐, 구글링해서 개념만 대강 잡고 공식문서로 시작하는게 꽤나 좋은 방법인 듯. 나만의 공부방법을 정립해가야할 때다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692602724897&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;목차&quot; data-og-host=&quot;catsbi.oopy.io&quot; data-og-source-url=&quot;https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0&quot; data-og-url=&quot;https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/teV7E/hyTIFtckvo/BvX8wKRi4IJ83kE8rHXJe0/img.png?width=2000&amp;amp;height=438&amp;amp;face=0_0_2000_438,https://scrap.kakaocdn.net/dn/gdoBc/hyTFjFj0wW/7imztqGp8tPmVZt2h007O1/img.png?width=2000&amp;amp;height=438&amp;amp;face=0_0_2000_438,https://scrap.kakaocdn.net/dn/ccwKq5/hyTIBYDkpf/RCCpOmMZwbDEd3qtU1KcM1/img.png?width=2000&amp;amp;height=713&amp;amp;face=0_0_2000_713&quot;&gt;&lt;a href=&quot;https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/teV7E/hyTIFtckvo/BvX8wKRi4IJ83kE8rHXJe0/img.png?width=2000&amp;amp;height=438&amp;amp;face=0_0_2000_438,https://scrap.kakaocdn.net/dn/gdoBc/hyTFjFj0wW/7imztqGp8tPmVZt2h007O1/img.png?width=2000&amp;amp;height=438&amp;amp;face=0_0_2000_438,https://scrap.kakaocdn.net/dn/ccwKq5/hyTIBYDkpf/RCCpOmMZwbDEd3qtU1KcM1/img.png?width=2000&amp;amp;height=713&amp;amp;face=0_0_2000_713');&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;목차&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;catsbi.oopy.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-security/reference/index.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692602745225&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Security :: Spring Security&quot; data-og-description=&quot;If you are ready to start securing an application see the Getting Started sections for servlet and reactive. These sections will walk you through creating your first Spring Security applications. If you want to understand how Spring Security works, you can&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-security/reference/index.html&quot; data-og-url=&quot;https://docs.spring.io/spring-security/reference/index.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-security/reference/index.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security :: Spring Security&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;If you are ready to start securing an application see the Getting Started sections for servlet and reactive. These sections will walk you through creating your first Spring Security applications. If you want to understand how Spring Security works, you can&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/Spring</category>
      <category>6.1.2</category>
      <category>AuthenticationEntryPoint</category>
      <category>spring security</category>
      <category>스프링 시큐리티</category>
      <category>원리</category>
      <category>인가</category>
      <category>인증</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/112</guid>
      <comments>https://jofestudio.tistory.com/112#entry112comment</comments>
      <pubDate>Mon, 21 Aug 2023 21:21:20 +0900</pubDate>
    </item>
    <item>
      <title>Oauth2 &amp;amp; JWT를 활용한 로그인&amp;amp;회원가입을 개발하며 마주하고 고민한 것들</title>
      <link>https://jofestudio.tistory.com/111</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소마 프로젝트를 진행하며, 내가 먼저 담당하게 된 기능은 로그인 및 회원가입 기능들이었다. 그 중에서도 소셜로그인 기능을 만들기로 했는데, 구글 &amp;amp; 카카오 &amp;amp; 애플에 대해서 만들기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;야생형 학습(일단 만들면서 그때그때 필요한 걸 찾아가는)이 좋다는 걸 알지만, Oauth라는 것 자체에 내가 무지했던 만큼 최소한 이게 어떤 건지, 어떤 원리로나마 굴러가는건지 파악을 한 뒤 기능개발에 들어가보기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아래 포스트는 당시 내가 Oauth에 대해 찾아보며 정리했던 포스트다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://jofestudio.tistory.com/105&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jofestudio.tistory.com/105&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691685167508&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;OAuth의 등장배경 및 기본적인 원리 &amp;amp; 과정 - top down형식으로 알아보자&quot; data-og-description=&quot;등장배경 우리가 만든 서비스가 유저를 대신해서 구글에서 제공하는 서비스에 뭔가를 하고 싶은 일들이 생겼던 거다. 가령 구글 캘린더에 일정등록을 우리가 만든 서비스가 해준다든가, 등등.. &quot; data-og-host=&quot;jofestudio.tistory.com&quot; data-og-source-url=&quot;https://jofestudio.tistory.com/105&quot; data-og-url=&quot;https://jofestudio.tistory.com/105&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/l3RcL/hyTBJjYBBj/9URqV0vZfuz2D0OR4gKBZ1/img.png?width=789&amp;amp;height=527&amp;amp;face=0_0_789_527,https://scrap.kakaocdn.net/dn/cb40Jc/hyTCGFVN9S/WyIInxwOnnDhd7e6QKbikK/img.png?width=789&amp;amp;height=527&amp;amp;face=0_0_789_527,https://scrap.kakaocdn.net/dn/dD9e0w/hyTBGtX92y/BKCoPkn8y9ErV3HASTtV1k/img.png?width=789&amp;amp;height=527&amp;amp;face=0_0_789_527&quot;&gt;&lt;a href=&quot;https://jofestudio.tistory.com/105&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jofestudio.tistory.com/105&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/l3RcL/hyTBJjYBBj/9URqV0vZfuz2D0OR4gKBZ1/img.png?width=789&amp;amp;height=527&amp;amp;face=0_0_789_527,https://scrap.kakaocdn.net/dn/cb40Jc/hyTCGFVN9S/WyIInxwOnnDhd7e6QKbikK/img.png?width=789&amp;amp;height=527&amp;amp;face=0_0_789_527,https://scrap.kakaocdn.net/dn/dD9e0w/hyTBGtX92y/BKCoPkn8y9ErV3HASTtV1k/img.png?width=789&amp;amp;height=527&amp;amp;face=0_0_789_527');&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;OAuth의 등장배경 및 기본적인 원리 &amp;amp; 과정 - top down형식으로 알아보자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;등장배경 우리가 만든 서비스가 유저를 대신해서 구글에서 제공하는 서비스에 뭔가를 하고 싶은 일들이 생겼던 거다. 가령 구글 캘린더에 일정등록을 우리가 만든 서비스가 해준다든가, 등등..&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jofestudio.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Oauth가 굴러가는 원리는 내 머릿속에선 다음과 같이 새겨졌다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 플랫폼에 들어가서 redirect uri 등등을 세팅&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 플랫폼(구글 등)에서 지정한 로그인페이지로 들어감&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;거기서 해당 플랫폼 계정으로 로그인하면 authorization code를 지급받는데, 사전 설정한 redirect uri를 통해 이걸 백엔드로 보내게 됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백엔드에선 해당 code를 통해 플랫폼으로부터 access token을 받음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그걸 통해서 회원정보도 조회하고.. 등등&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우리 프로젝트는 자체적으로 구축할 db에 회원정보를 저장할 것이다. 따라서 백엔드에서 4번 과정을 통해 받은 access token을 활용해 플랫폼으로부터 회원정보를 받아오고, 해당 회원정보를 갖는 회원이 실제로 우리 db에 있는지를 판단해서 로그인 및 회원가입을 만들면 되겠다는 그림이 머릿속에 그려졌다. 네이티브 앱은 세션 로그인이 불가능하기 때문에, 해당 회원에 대한 JWT를 만들어서 플러터 단으로 주면 되겠다는 생각도 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;그러면 일단 저 순서대로 만들어가면 되겠네?&quot;&lt;/b&gt;&lt;/span&gt; 라는 생각이 들었다. 그러나 문제는.. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;내가 모든 게 처음이라는 것&lt;/b&gt;&lt;/span&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;앱에서는 authorization code만 받는 걸 어떻게 하지?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;구현방법은 조금씩 달라도, 결국 저 순서를 따르는 식으로들 만들겠지!&quot;&lt;/b&gt;&lt;/span&gt; 라고 처음에 생각했다. 각 플랫폼 별로 지정한 로그인 페이지를 띄우려면 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;웹뷰를 띄워야겠다&lt;/b&gt;&lt;/span&gt;고 생각했고, 웹뷰 띄우는 방법을 먼저 검색해야 한다는 갈피가 잡혔다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 얼마 지나지 않아.. 구글에서는 보안 정책상의 이유로 웹뷰를 통한 구글로그인을 막아놨다는 걸 알게 됐다. user agent를 변경하여 이를 우회할 수도 있다고 하나, 다른 것도 아니고 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;앱의 보안과 사용성에 부정적인 영향을 끼칠 수 있어서 막아둔 걸 굳이 우회해서 이용할 필요는 없다&lt;/b&gt;&lt;/span&gt;고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;..그러면 다른 방법을 찾아야 한다! 일단 플러터 구글로그인 예제를 검색해봤다. google_sign_in이라는 패키지를 통해 구글로그인을 할 수 있다는 걸 알게 되고, 써봤다. 매우 쉽게 google에서 지급한 access token을 플러터 단에서 받을 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 뭔가 이상한 걸 느꼈다. 내가 이해한 Oauth2 프로토콜은 사용자 측에서 redirect uri를 통해 백엔드로 authorization code를 보내면 백엔드가 그걸 통해서 resource server(ex: google)로부터 access token을 받아야 한다. 그러나 지금은 버튼을 딸깍 하고 누르니 바로 플러터 단에서 google의 access token이 받아져(?)버렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 백엔드로 authorization code를 넘기는 것은 내가 그린 그림이었을 뿐, 프론트 쪽에서 토큰을 받는 식으로 처리해도 상관은 없다. 애당초 Oauth2 프로토콜에서 말하는 &quot;client&quot;라는 것도 엄밀히 말하면 우리가 구축하는 서비스를 말하는 것이지 백엔드만을 말하는 게 아니니까. 따라서 이 상태, 즉 프론트에서 토큰을 받은 상태에서 그대로 백엔드 단으로 access token을 넘긴 뒤 백엔드에서 그걸 통해서 유저 정보를 조회하고 JWT를 만드는 식으로 가도 될 것이다. 그러나 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;access&lt;/b&gt;&lt;/span&gt; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;token자체를 백엔드로 넘긴다? 이거 탈취당하면 어떡할건데?&lt;/b&gt; &lt;/span&gt;라는 생각이 머리를 스쳤다.&amp;nbsp; 그리고 우리 서비스에서 사용할 커스텀 JWT를 만들기 위해 access token을 백엔드로 넘기는 거면 백엔드에서 그 access token을 가지고 resource server에서 회원정보 가져올 텐데, 어차피 플러터 쪽에서(즉 프론트에서) access token을 가지고 있는거면 플러터에서 바로 resource server로부터 회원정보를 가져오고 그걸 백엔드로 넘기는게 더 효율적일 수도 있다는 생각도 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 access token이 아닌 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;authorization code를 백엔드로 보내게 되는게 보안상 안전&lt;/b&gt;&lt;/span&gt;하다는 생각이 들었다. 그래서 어떻게든 authorization code를 받아오는 방법을 찾아봐야겠다는 생각을 하고 여기저기 쑤셔대기 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 구글링해서 나오는 예제는 죄다 google_sign_in패키지를 활용해서 바로 access token을 받아오는 것들이었고,, 거의 대부분이 firebase와 연계해 인가/인증을 하는 예제들이었다. &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;나는 따로 회원정보를 자체적으로 구축할 db에 저장하고 이를 활용할 것(외래키로 사용 등..)이기 때문에, 최대한 firebase를 쓰는 예제들은 지양하고 있었다. 물론 firebase를 쓴다 해도 그와는 별개로 우리 플젝에서 쓸 db에도 어찌어찌 회원데이터를 넣을 순 있겠으나, firebase와 자체db의 정보를 계속 동기화하는 것 등등 신경쓸 것이 너무나 많아질 것이란 생각도 들었고, 내가 만들 백엔드를 통해 인가/인증을 하지 못하게 될 거라는 점도 맘에 안 들었다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;결국 authorization code만 따로 쏙 빼고 싶으면 웹뷰를 써야 하는데.. 일단 아까 말했다시피 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;구글에서는 웹뷰를 통한 로그인을 막아둔 상태&lt;/b&gt;&lt;/span&gt;. 그리고 이걸 배제한다고 해도 문제가 있었다. redirect uri를 통해 백엔드로 바로 authorization code를 넘겨야 하는데, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;웹뷰에서 redirect uri를 통해 백엔드로 보낼 수가 있는가&lt;/b&gt;&lt;b&gt;?&lt;/b&gt;&lt;/span&gt;라는 의문점이 든거다. 즉 나는 로컬환경에서 개발 중인데, 노트북에서 에뮬레이터로 띄운 휴대폰 기기의 웹뷰에서 redirect uri로 localhost뭐시기를 했을 때.. 내 로컬 &quot;노트북&quot;에서 돌아가는 스프링부트로 authorization code가 오는가..의 문제. 당시에는 웹뷰 입장에선 자신을 돌리고 있는건 핸드폰 기기인 셈이니 얘한테 있어서 localhost는 기기를 말하지 않겠냐는 생각이 머리에서 돌아가게 된 거다. 이에 대한 해결책으론 배포를 해서 배포된 주소로 보내든가.. 아니면 내 노트북의 ip주소로 보내든가해야 하는데, 두 방법 모두 썩 좋은 해결책은 아니라는 생각이 들었고.. 무엇보다도 애당초 구글은 웹뷰를 통한 로그인을 막아뒀다!&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;(나중에 에뮬레이터의 웹뷰에서 localhost를 때려도 노트북으로 http를 보낼 수 있다는 걸 알게 되긴 했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt; &lt;a href=&quot;https://learn.microsoft.com/ko-kr/xamarin/cross-platform/deploy-test/connect-to-local-web-services&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://learn.microsoft.com/ko-kr/xamarin/cross-platform/deploy-test/connect-to-local-web-services&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;923&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/devKPj/btsrcqS2K2Y/BuI6slKccjmiZdxQMiTVr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/devKPj/btsrcqS2K2Y/BuI6slKccjmiZdxQMiTVr1/img.png&quot; data-alt=&quot;원래는 이렇게 만들고 싶었다.. (파워포인트로 후루룩 만듦ㅋㅋ)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/devKPj/btsrcqS2K2Y/BuI6slKccjmiZdxQMiTVr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdevKPj%2FbtsrcqS2K2Y%2FBuI6slKccjmiZdxQMiTVr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;923&quot; height=&quot;607&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;원래는 이렇게 만들고 싶었다.. (파워포인트로 후루룩 만듦ㅋㅋ)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;머리가 아파졌고,, 그냥 카카오 로그인이나 먼저 만들까란 생각으로 카카오 로그인 공식문서를 열었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 열자마자..!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;809&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btwDP1/btsrkRJJxC6/KrE4vxmzUlbyrgLNy8xc8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btwDP1/btsrkRJJxC6/KrE4vxmzUlbyrgLNy8xc8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btwDP1/btsrkRJJxC6/KrE4vxmzUlbyrgLNy8xc8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtwDP1%2FbtsrkRJJxC6%2FKrE4vxmzUlbyrgLNy8xc8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;545&quot; height=&quot;270&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;401&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앱 서비스에서는 Redirect 방식 사용불가라는 말이 떡하니 있었다. 게다가 얘도 클라이언트단에서 인가 코드와 토큰 발급을 모두 처리하는 방식을 가지고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;나는 우리 프로젝트에서 사용할 소셜플랫폼인 카카오, 구글, 애플은 동일한 형태의 로그인을 만들고 싶었다(셋 다 토큰 자체를 백으로 넘기든가, 아니면 셋 다 authorization code를 백으로 넘기든가..). &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;근데 어차피 구글은 웹뷰를 통한 로그인을 막아놨고, 카카오도 공식문서피셜로는 네이티브 앱 환경에서는 Redirect 방식을 사용할 수 없다고 공지를 내렸었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그러면 결단을 내릴 차례.. 하는 수 없이 그냥 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;플러터에서 토큰 넘기자&quot;로 결정&lt;/b&gt;&lt;/span&gt;하게 됐다. 프로젝트에서 주어진 시간도 많지 않았기 때문에, authorization code를 보내는 방법을 더 알아내는 것보단 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;일단 로그인 자체를 만드는 것에 더 포커스&lt;/b&gt;&lt;b&gt;를 두기로 한 것&lt;/b&gt;&lt;/span&gt;도 있었다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;AWS Cognito와의 사투&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그렇게 플러터 단에서 토큰을 받아서 백엔드로 넘기는 로직을 만들었을 때쯤, 연수생 동기와 Oauth에 대한 얘기를 주고 받았었다. 근데 그 친구가 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;firebase&lt;/span&gt;를 써서 소셜로그인을 만들었다는 얘기를 했고, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;firebase&lt;/span&gt;를 사용해서도 자체 백엔드에서 인가인증이 가능하다는 얘기를 해줬다!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl&quot;&gt;https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1692202502164&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;ID 토큰 확인
  
 &amp;nbsp;|&amp;nbsp; Firebase Authentication&quot; data-og-description=&quot;Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 ID 토큰 확인 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Firebase 클라이&quot; data-og-host=&quot;firebase.google.com&quot; data-og-source-url=&quot;https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl&quot; data-og-url=&quot;https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl&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;ID 토큰 확인 &amp;nbsp;|&amp;nbsp; Firebase Authentication&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 ID 토큰 확인 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Firebase 클라이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;firebase.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;사실 나는 이전에 firebase를 잠깐 써봤을 때 걔네가 지들이 알아서 인가인증을 다 해줬던 걸로 기억해, firebase를 쓰면 내 백엔드에서 인가인증하는 식으로 못 만든다고 생각해서 지금까지 지양했던건데,, 역시 난 우물 안 개구리였구나라는 생각이 들었다.. ㅎㅎㅎ;;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;그 때 한창 소마에서 지원해주는 AWS 심화교육(Architection on AWS)를 듣고 있던 터였는데, 우리 얘기를 듣고 있던 강사님께서 AWS에서 제공하는 Cognito란 걸 사용하면 똑같은 걸 할 수 있다고 말씀해주셨다. 그 때 삘이 꽂혀서,, ㅎㅎ; 갑자기 코그니토를 통한 소셜로그인을 엄청나게 검색하고 따라해봤다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;뭐. 결론적으로 말하면 이 방법은 성공하지 못 했다. AWS Amplify를 통해 Flutter에서 이것저것 시도를 해봤고,, 공식문서도 그대로 따라하는걸 10번 정도는 한 것 같다. 근데 자꾸 에러가 났다 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 시간 문제에 다시 직면했고, 타협을 해야 할 지점이 왔다. 로그인/회원가입을 만드는 것에서 더 이상 시간을 끌 수 없던 노릇이라, AWS Cognito를 사용하는 방식을 버리기로 했다. 그러면 firebase를 그냥 쓸까?라는 생각이 들었지만, 내가 직접 JWT를 만들어서 인가인증을 만들어보는게 더 낫겠다는 생각이 들었다. 이전에 jwt를 활용한 로그인을 따라하면서 어떻게 만들어본 적은 있지만, 뭔지 알고 만들었던 건 아니었어서 이번에 각잡고 만들어봐야겠다는 마음이 들었고.. 이게 어쨌든 좀 더 공부가 될 것 같았기 때문.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 다음과 같은 흐름으로 만들어보기로 했다. 탕탕탕!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;886&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cym2AI/btsrr1qZw6K/Jk2kBJUfM20KsVsEhX8jM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cym2AI/btsrr1qZw6K/Jk2kBJUfM20KsVsEhX8jM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cym2AI/btsrr1qZw6K/Jk2kBJUfM20KsVsEhX8jM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcym2AI%2Fbtsrr1qZw6K%2FJk2kBJUfM20KsVsEhX8jM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;592&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;스프링 시큐리티 &amp;amp; JWT로 인가 인증 만들기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;member테이블을 만들고, db에 회원정보가 있는지 없는지 조회하는 건 금방 만들었다. 근데 스프링 시큐리티라는 놈이 인가인증을 어떻게 해주는건지, 그리고 스프링 시큐리티가 JWT를 통해서는 어떤 식으로 인가인증을 하는지는 잘 몰랐다. 그래서 일단 이놈들의 이론적인 부분에 대해(?) 먼저 짚고 넘어가기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jofestudio.tistory.com/112&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jofestudio.tistory.com/112&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692620751955&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 Security] 스프링 시큐리티의 인증/인가 과정(6.1.2버전 기준, with 공식문서)&quot; data-og-description=&quot;읽기 전에 : 해당 글은 스프링 시큐리티 6.1.2버전 기준으로, 읽는 시점에 따라 다른(deprecate됐다든지) 내용일 수도 있습니다. 어떠한 요청을 보냈을 때, 요청을 보낸 이에 대한 인가/인증이 수반돼&quot; data-og-host=&quot;jofestudio.tistory.com&quot; data-og-source-url=&quot;https://jofestudio.tistory.com/112&quot; data-og-url=&quot;https://jofestudio.tistory.com/112&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6rxhK/hyTFgBVFxN/i5YZp6o7J5GtmdzsRe8WCk/img.png?width=486&amp;amp;height=179&amp;amp;face=0_0_486_179,https://scrap.kakaocdn.net/dn/mavlh/hyTICwxZcx/arpnLw9ckktDHvcZORDUbK/img.png?width=486&amp;amp;height=179&amp;amp;face=0_0_486_179,https://scrap.kakaocdn.net/dn/f4m6C/hyTIQ2BcBO/jjugzOp8liwqavfEzwsOl0/img.png?width=1280&amp;amp;height=687&amp;amp;face=0_0_1280_687&quot;&gt;&lt;a href=&quot;https://jofestudio.tistory.com/112&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jofestudio.tistory.com/112&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6rxhK/hyTFgBVFxN/i5YZp6o7J5GtmdzsRe8WCk/img.png?width=486&amp;amp;height=179&amp;amp;face=0_0_486_179,https://scrap.kakaocdn.net/dn/mavlh/hyTICwxZcx/arpnLw9ckktDHvcZORDUbK/img.png?width=486&amp;amp;height=179&amp;amp;face=0_0_486_179,https://scrap.kakaocdn.net/dn/f4m6C/hyTIQ2BcBO/jjugzOp8liwqavfEzwsOl0/img.png?width=1280&amp;amp;height=687&amp;amp;face=0_0_1280_687');&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 Security] 스프링 시큐리티의 인증/인가 과정(6.1.2버전 기준, with 공식문서)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;읽기 전에 : 해당 글은 스프링 시큐리티 6.1.2버전 기준으로, 읽는 시점에 따라 다른(deprecate됐다든지) 내용일 수도 있습니다. 어떠한 요청을 보냈을 때, 요청을 보낸 이에 대한 인가/인증이 수반돼&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jofestudio.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(스프링 시큐리티 원리에 대해 공부하고 정리한 글. 실제로 개발할 때는 완전 딥하게는 공부하지 못했고, 로그인/회원가입을 일단 만든 뒤에 몰랐던 부분 더 찾아본 뒤에 정리해서 작성한 글이다)&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스프링 시큐리티가 인가인증을 해주는 &quot;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기본적인&lt;/b&gt;&lt;/span&gt;&quot; 방식은 내 머릿속엔 다음과 같이 새겨졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;유저가 로그인을 시도(with username &amp;amp; password)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;필터에서 DB에 있는 유저인지 체크함(username을 활용)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;있는 놈이면 &quot;그 유저의 정보를 담은 문서&quot; = UserDetails를 만듦&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 UserDetails의 내용과 유저가 로그인할 때 보낸 정보(password)를 비교.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;같다면 통행증 = Authentication을 끊어줌. 통행증에는 누군지(=principal이라 부르고, 얘 역할을 UserDetails가 함), 그리고 뭘 할 수 있는 앤지(=GrantedAuthority)가 적혀있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그걸 SecurityContextHolder에 저장하면서 세션에도 저장!&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;유저에게 세션id를 주고 이후 요청들은 이걸로 인가인증하는 거임&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한마디로 SecurityContextHolder라는 곳에 Authentication이라는 통행증을 발급해서 넣어둔다. 그 후 나중에 이렇게 넣어둔 Authentication이 있는지를 봐서 &quot;인증&quot;을 하고, 이 통행증에 써져 있는 권한(GrantAuthority)를 보고 &quot;인가&quot;를 하는 식으로 Spring Security가 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;난 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;jwt를 활용&lt;/b&gt;&lt;/span&gt;해 인가인증을 할 거다. 그럼 위 과정을 토큰방식으로 바꾼다면 어떨까? 토큰방식으로 바꿔도 뭐가 됐든 간에 Authentication을 만든 다음 그걸 SecurityContextHolder에 넣는 것은 동일할 것이다. 그 후 SecurityContextHolder에 Authentication이 들어있는지를 보며 &quot;인증&quot;을 하고, 나중에 Authentication을 꺼내 GrantedAuthority를 보고 &quot;인가&quot;를 할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 대략적인 흐름이 머리 속에 잡혔고, 그 후&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구글링을 통해 스프링 시큐리티와 jwt관련된 예제들을 살폈다. 나름 원리를 알고 나니(?) 예제들을 보면서 &quot;아 여기선 뭘 하고 있는 거구나&quot; 이런 게 느껴졌다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음과 같은 흐름으로 만들기로 했다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 로그인 성공하면 jwt반환(Access token) -&amp;gt; 여긴 유저의 이메일 정보를 담는다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트가 헤더에 Access Token을 담아 요청을 보냄&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;헤더에서 Access Token 추출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Access Token 검증(유효기간 만료 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 Access Token으로부터 Authentication을 만들고 이를 SecurityContext에 꽂아야 한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 Access Token에서 이메일 추출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 이메일을 갖는 유저의 UserDetails를 만듦&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 UserDetails를 활용해 Authentication을 만든다(principal은 UserDetails가, grantedAuthority는 UserDetails.getAuthorities를 활용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇게 만든 Authentication을 SecurityContext에 꽂는다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그림으로 보면 다음과 같다. jwtFilter, jwtProvider 등등은 내가 정한 클래스들의 이름이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1787&quot; data-origin-height=&quot;949&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AWzib/btsrIcz1RUT/LV6LqAipeg70GBcYVpi3m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AWzib/btsrIcz1RUT/LV6LqAipeg70GBcYVpi3m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AWzib/btsrIcz1RUT/LV6LqAipeg70GBcYVpi3m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAWzib%2FbtsrIcz1RUT%2FLV6LqAipeg70GBcYVpi3m0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1787&quot; height=&quot;949&quot; data-origin-width=&quot;1787&quot; data-origin-height=&quot;949&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 과정에서 잠깐 고민했던 부분은 UserDetails의 구현체를 어떤 방식으로 만드는가였다. User(내 프로젝트는 Member) 클래스가 UserDetails를 구현하는 식으로 만들 수도 있고, 정말 아예 User클래스와 CustomUserDetails(UserDetails의 구현체 클래스)를 별도로 둘 수도 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;나는 후자로 했다! 물론 UserDetails를 User가 구현하도록 하는게 뭔가 더 편할 것 같긴 하지만, UserDetails의 원래 용도 자체는 &quot;사용자의 정보가 담긴 문서&quot;인 만큼, User가 UserDetails를 구현하면 유저 자체가 통째로 사용되는 것이니 조금 번거롭더라도 UserDetails를 구현하는 별도의 커스텀 클래스를 만드는게 보안상 좀 더 안전하리라 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아 그리고 UserDetails를 받은 뒤 Authentication을 만드는 과정에서, 요 Authentication이란 놈은 인터페이스여서 구현체를 만들어서 써야 하는데, 이건 UsernamePasswordToken이란 놈이 사용된다. 근데 이 놈은 생성자가 2개다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692633354875&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
	return new UsernamePasswordAuthenticationToken(principal, credentials);
}

public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
	Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities) {
	return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;전자는 인증되지 않은 상태의 Authentication을 만드는 거고, 후자는 인증된 상태의 Authentication을 만드는 생성자이다. 전자는 도대체 왜 있는거지? 싶었는데, 알고보니 username &amp;amp; password로 하는 전통의 원조(?) 로그인에서는 전자를 통해서 인증되지 않은 상태로 만든 뒤 UserDetails와 비밀번호를 대조하는.. 뭐 그런 과정들을 거친 뒤에 인증된 상태로 바꿔주는 과정이 있는 거였다. ㅇㅇ&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 지금 내가 하는 과정에서는 굳이 그렇게까지 할 이유가 안 보였다. jwt가 클라이언트로부터 온다는 거 자체가 이미 이전에 로그인을 통한 신원 확인이 끝나서 클라이언트가 서버로부터 jwt를 받은 것이기 때문.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;인증/인가 실패 시 처리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증/인가를 할 수 있도록 Authentication을 SecurityContext에 꽂아넣는 건 만들었다. (비하인드 : 요로코롬 SecurityContext에 Authentication을 꽂아둬야 인증한 것으로 취급된다는 걸 당시엔 명확하게 몰라서 무수히 많은 로그를 찍어봤었 건 비밀). ok. SecurityContext에 Authentication이 없으면 인증에 실패한 것으로 처리되는 것도 알겠다. 그러면 인증이 실패했을 때와 인가에 실패했을 때에 대한 처리는 어떻게 하는가? 예를 들어, Access Token의 검증에 실패하면 그건 인증 실패로 처리해야 할테니까.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일단 인증실패는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;AuthenticationEntryPoint&lt;/b&gt;&lt;/span&gt;라는걸 통해서 가능했다! 인증에 실패하면 AuthenticationException이 발생하는데, 이걸 AuthenticationEntryPoint에서 처리한다. 커스텀하게 AuthenticationEntryPoint를 만들어 클라이언트에게로 가는 Response를 조작할 수 있는데, 여기서 고민이 생겼다. 클라이언트로 가는 Response들에 세부적인 메시지를 내가 달아주고 싶은데, 그걸 어떻게 하지? 라는 고민. 이게 무슨 말이냐?&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692686755536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface AuthenticationEntryPoint {
	/**
	 * @param request that resulted in an AuthenticationException
	 * @param response so that the user agent can begin authentication
	 * @param authException that caused the invocation
	 */
	void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
			throws IOException, ServletException;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요렇게 AuthenticationEntryPoint의 commence메서드를 통해 AuthenticationException을 처리한다. 그리고 내가 만든 jwtFilter에서는 다양한 이유들로 인증 실패를 낼 수 있다. Access Token의 만료기간이 지났다든지, 유효하지 않는 서명을 사용했다느든지 등등.. 그럴 때마다 단순히 &quot;인증 실패요~&quot;라는 것보다는 클라이언트에게 다양한 메시지(뭐 땜에 실패했어요~)를 전달하고 싶은데, 그 오류메시지(?)들을 어떻게 AuthenticationEntryPoint로 넘기는지를 고민하게 된 거다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AuthenticationException도 추상 클래스이기 때문에, 커스텀하게 이를 구현하는 클래스를 만들어 내가 원하는 오류 문구를 담을 수도 있을 것이다. 그러면 jwtFilter에서 이렇게 커스텀하게 만든 AuthenticationException을 만들어 던진다면 어떨까? 싶었는데.. 일단 지금와서 돌이켜보면 당시에는 이 생각을 못 했다. 지금이야 돌이켜보니 일단 만드는 과정에서 부족하지만 나름대로의 지식이 생기긴 했지만.. 당시엔 뭐 어떻게되는지 모르겠지만 SecurityContext에 Authentication을 안 꽂아두면 뒷단에 있는 필터에서 AuthenticationException이 발생하고, AuthenticationEntryPoint가 이를 처리하네! 정도만 알던 상태라..&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지금 돌이켜보면 jwtFilter에서 커스텀하게 만든 AuthenticationException을 던져도 그게 ExceptionTranslationFilter에서 처리되지 않는다는 걸 안다. 왜냐하면 ExceptionTranslationFilter는 doFilter를 통해 자기보다 뒷단의 필터(AuthorizationFilter)에서 일어나는 AuthenticationException만 처리하는데, &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;내가 만든 jwtFilter는 ExceptionTranslationFilter보다 앞단에 위치하기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: justify;&quot;&gt;암튼 그래서 당시엔 다른 사람들이 만든 코드들을 살피면서 방법을 찾다가, 나름 기똥찬(?) 방법을 발견했다. 그건 바로 request에 에러메시지를 저장하는 것! 자세히보면 commence메서드가 AuthenticationException뿐만 아니라 request와 response도 파라미터로 받고 있는걸 볼 수 있다. 즉 jwtFilter쪽에서 request에 에러메시지를 세팅해두면, 나중에 AuthenticationEntryPoitn의 commence메서드에선 파라미터로 받은 request에 들어가있는 에러메시지를 꺼낼 수 있는 거다!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692688527435&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 내가 만든 JwtFilter 내부..
try {
	jwtProvider.validateAccessToken(accessToken);
} catch (CustomJwtException ex) {
	request.setAttribute(&quot;exception&quot;, ex.getMessage());
	return;
}
// .. 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692688705698&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 커스텀 AuthenticationEntryPoint
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
						AuthenticationException authException)
			throws IOException, ServletException {
		// request로부터 꺼내기
		String errorMessage = (String) request.getAttribute(&quot;exception&quot;);
		setResponse(response, errorMessage);
	}
	// ..생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러면 인가실패는 어떻게 처리하는가? 인가에 실패하면 AccessDeniedException이 던져지며, 이는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;AccessDeniedHandler&lt;/b&gt;&lt;/span&gt;를 통해 처리할 수 있다. 이 핸들러또한 커스텀하게 만들 수 있어서, response를 내가 원하는 식으로 조작 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인가 실패는 그럼 어떤 상황에서 이뤄지는가? 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AuthorizationFilter에서 진행됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SecurityContextHolder에서 Authentication을 가져옴. 못 가져오면 AuthenticationException&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Authorities들, 즉 사용자의 권한 목록을 꺼냄&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시큐리티 설정에서 hasRole을 사용했다면, 해당 메서드의 인자로 넘긴 값들과 사용자의 권한 목록을 비교. 참고로 hasRole의 인자로 넘긴 값들에 대해 자동으로 ROLE_이라는 prefix를 붙여줌. 예를 들어 hasRole(&quot;USER&quot;) -&amp;gt; 사용자의 권한 목록에 ROLE_USER가 있어야 한다는 거&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일치하는게 있으면 ok. 없으면 AccessDeniedException&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 난 원래 Member엔티티들이 가지는 Role을 USER, ADMIN이런 식으로 줬어서.. 급하게 앞에 ROLE_들을 붙이는 방식으로 수정했었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이후 Configuration파일에서 다음과 같은 설정을 통해 커스텀하게 만든 AuthenticationEntryPoint, AccessDeniedHandler를 등록해줬다. 당연한 소리지만 이렇게 커스텀하게 만든 애들을 등록하지 않으면 디폴트로 있는 애들이 쓰인다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692689942266&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class SecurityConfig {
    // ... 생략

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // ... 생략
                ).exceptionHandling((exceptionHandling) -&amp;gt; {
                    exceptionHandling.authenticationEntryPoint(new JwtAuthenticationEntryPoint());
                    exceptionHandling.accessDeniedHandler(new JwtAccessDeniedHandler());
                });

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;토큰 재발급 처리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Access Token &amp;amp; Refresh Token을 발급해서 클라이언트에게 넘기는 건 끝났다. 클라이언트(플러터) 단에서는 Access Token과 Refresh Token을 보관소에 가지고 있도록 했는데, 로그인 유지 기능을 위해 앱 접속 시 자동으로 보관소에 들어있는 토큰들을 갱신해주기로 했다. 난 Access Token의 유효기간을 1시간, Refresh Token의 유효기간을 30일로 만들었으니 앱을 접속을 안 하고 있었다고 해도 30일 내로 접속한다면 로그인이 유지된 채로 앱을 이용할 수 있는 거다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일단 당연히 Refresh Token을 서버로 보내면 걔를 검증(유효기간 만료 등)해야 한다. 그럼 이 검증만 하고 재발급을 바로 해주면 되나? 싶었지만 좀 더 보안을 위한 방법이 있었다. 바로 Refresh Token원본을 DB에 저장하고, 클라이언트로부터 온 Refresh Token을 그와 대조하는 것.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 다음과 같은 흐름이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;2624&quot; data-origin-height=&quot;1764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OTVLd/btsrYyhR2wf/oQPydQk7TcIHkraJgA1DzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OTVLd/btsrYyhR2wf/oQPydQk7TcIHkraJgA1DzK/img.png&quot; data-alt=&quot;소셜로그인 과정.. Resource Server에서 토큰받아오고 그런 과정은 과감하게 생략했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OTVLd/btsrYyhR2wf/oQPydQk7TcIHkraJgA1DzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOTVLd%2FbtsrYyhR2wf%2FoQPydQk7TcIHkraJgA1DzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2624&quot; height=&quot;1764&quot; data-origin-width=&quot;2624&quot; data-origin-height=&quot;1764&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소셜로그인 과정.. Resource Server에서 토큰받아오고 그런 과정은 과감하게 생략했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 DB에 Refresh Token을 보관상으로 얻는 구체적인 이점들은 도대체 뭘까? &lt;s&gt;이거 디비에 둘 필요 없이 그냥 검증만 해주면 되는거 아니야? 왜 굳이 DB에 저장해? (라고 생각했던 건 본인)&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 단순히 Refresh Token을 검증만 하고 재발급하는 수준이라면 -&amp;gt; 악의적 사용자도 Refresh Token 탈취해서 악용이 가능. 하지만 DB에 Refresh Token을 저장하는 구조라면? 악의적 사용이 보고됐을 때 DB에 저장된 Refresh Token을 삭제함으로써, 악의적 사용자가 Refresh Token을 보내도 검증은 되겠지만 DB에 Refresh Token이 없으니 재발급을 안해주는 식으로 대응이 가능하다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Access Token을 발급받을 때 Refresh Token도 함께 발급받으면서 DB에 기존에 있던 Refresh Token을 삭제한 뒤 새로 발급받은 Refresh Token을 저장하는 방식이면 -&amp;gt; 악의적 사용자가 어찌저찌 Refresh Token을 탈취해도, 유저가 다시 로그인하거나 하면 DB에 있던 Refresh Token도 갱신되는 셈 -&amp;gt; 악의적 사용자가 지가 가지고 있던 Refresh Token을 보내도 물론 검증은 통과하겠지만 DB에는 해당 Refresh Token은 삭제됐으니 재발급을 안해주는 식의 대응이 가능해진다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요 내용은 다음 글에서 좀 더 자세히 확인 가능하다. 영어로 된 글이니 놀라지 말 것..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://betterprogramming.pub/should-we-store-tokens-in-db-af30212b7f22&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://betterprogramming.pub/should-we-store-tokens-in-db-af30212b7f22&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692693997372&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;3 Scenarios Where You Can Store JWT Token in Your DB&quot; data-og-description=&quot;Know when and why&quot; data-og-host=&quot;betterprogramming.pub&quot; data-og-source-url=&quot;https://betterprogramming.pub/should-we-store-tokens-in-db-af30212b7f22&quot; data-og-url=&quot;https://betterprogramming.pub/should-we-store-tokens-in-db-af30212b7f22&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3qBEI/hyTINrso52/IuX8Bdzk9vvilgVN5LEn7K/img.jpg?width=1200&amp;amp;height=755&amp;amp;face=0_0_1200_755,https://scrap.kakaocdn.net/dn/bdAI84/hyTIRUUHUA/KUljpxpMzxJKk3zvvmRUf1/img.jpg?width=1358&amp;amp;height=904&amp;amp;face=0_0_1358_904,https://scrap.kakaocdn.net/dn/utL3Q/hyTIRN9XqQ/hkclfxvKPsQSyjXi02jEa0/img.png?width=1024&amp;amp;height=766&amp;amp;face=0_0_1024_766&quot;&gt;&lt;a href=&quot;https://betterprogramming.pub/should-we-store-tokens-in-db-af30212b7f22&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://betterprogramming.pub/should-we-store-tokens-in-db-af30212b7f22&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3qBEI/hyTINrso52/IuX8Bdzk9vvilgVN5LEn7K/img.jpg?width=1200&amp;amp;height=755&amp;amp;face=0_0_1200_755,https://scrap.kakaocdn.net/dn/bdAI84/hyTIRUUHUA/KUljpxpMzxJKk3zvvmRUf1/img.jpg?width=1358&amp;amp;height=904&amp;amp;face=0_0_1358_904,https://scrap.kakaocdn.net/dn/utL3Q/hyTIRN9XqQ/hkclfxvKPsQSyjXi02jEa0/img.png?width=1024&amp;amp;height=766&amp;amp;face=0_0_1024_766');&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;3 Scenarios Where You Can Store JWT Token in Your DB&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Know when and why&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;betterprogramming.pub&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>PROJECT/개발일지</category>
      <category>JWT</category>
      <category>OAuth</category>
      <category>개발일지</category>
      <category>로그인</category>
      <category>회원가입</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/111</guid>
      <comments>https://jofestudio.tistory.com/111#entry111comment</comments>
      <pubDate>Sat, 12 Aug 2023 17:23:18 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] SSH 포트포워딩 및 SSM을 통해서 로컬에서 Private Subnet에 있는 RDS 접근하기</title>
      <link>https://jofestudio.tistory.com/110</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소마 프로젝트를 진행하면서, 인프라 쪽을 내가 어느 정도 담당하고 있어서&lt;s&gt;(사실 담당한다고 하기에도 민망할 정도로 기본적인 부분들만 담당하긴 하지만)&lt;/s&gt; RDS를 띄워보기로 했다. Private Subnet에 RDS를 띄우기로 했는데, 문제는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;로컬에서 Private Subnet에 어떻게 접근하는가?&quot;&lt;/b&gt;&lt;/span&gt; 였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Private Subnet은 외부와 단절된 네트워크로, 외부에서 직접 접근이 불가능하다. NAT Gateway를 둬서 Private Subnet에서 외부로 트래픽을 보낼 순 있어도, 외부에서 먼저 Private Subnet으로 트래픽을 보낼 순 없다. VPN등을 통해 직접 로컬에서 Private Subnet으로 연결할 순 있으나, 비용이 무시무시하다는 소문이 들려서 다른 방법을 찾아보기로 했다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;당장 떠오르는 방법은 Public Subnet에 EC2를 둔 후, ssh로 EC2에 원격으로 연결한 다음 거기서 Private Subnet으로 접근하는 것. 같은 VPC안의 애들끼리는 서로 통신이 가능하기 때문에 이 방법을 쓸 수 있을 것이다. 이렇게 외부에서 내부 네트워크로의 안전한 접근을 제어하기 위해 사용되는 중간 서버를 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Bastion Host&lt;/b&gt;&lt;/span&gt;라고 부른다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1268&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D3B6f/btsqCreWDCn/NgRr8uLIjSipVmtXHKzxVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D3B6f/btsqCreWDCn/NgRr8uLIjSipVmtXHKzxVk/img.png&quot; data-alt=&quot;로컬에서 ssh로 Public Subnet의 EC2에 원격으로 연결한 뒤, 거기서 Private Subnet의 RDS로 직접 쿼리를 날릴 순 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D3B6f/btsqCreWDCn/NgRr8uLIjSipVmtXHKzxVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD3B6f%2FbtsqCreWDCn%2FNgRr8uLIjSipVmtXHKzxVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;364&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로컬에서 ssh로 Public Subnet의 EC2에 원격으로 연결한 뒤, 거기서 Private Subnet의 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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하면 로컬에서 Private Subnet에 있는 RDS로 create등의 쿼리를 작성해 날릴 수 있다. 그러나 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;얘를 로컬에서 돌리고 있는 스프링부트와 어떻게 연결하는가&quot;&lt;/b&gt;&lt;/span&gt;가 숙제. 스프링부트에서 만들어지는 쿼리를 터미널에 인풋으로 주도록 내가 직접 구축을 해야 하나..? 그건 너무 에바같은데..&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 때 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;ssh 포트 포워딩(다른 말로는 ssh 터널링)&lt;/b&gt;&lt;/span&gt;을 통해 이 문제를 해결할 수 있다. ssh 포트포워딩이란 ssh를 사용해 로컬 머신과 원격 서버 사이에서 포트 전달을 설정하는 것으로, 이를 통해 로컬에서 특정 포트로 트래픽을 보내면 ssh터널(ssh를 통해 연결된 통로라고 생각)을 통해 원격서버의 특정 포트로 보낼 수가 있다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;즉 내가 로컬의 3306포트와 원격 서버의 3306포트를 포트포워딩하면, 내가 로컬의 3306포트로 보내는 트래픽을 원격 서버의 3306포트로 보낼 수가 있는 거&lt;/b&gt;&lt;/span&gt;다. (참고로 이 예시는 로컬 포트 포워딩에 해당하며, 리모트 포트 포워딩을 하면 원격 서버에서 특정 포트로 들어오는 트래픽을 내 로컬 머신의 포트에서 받을 수 있다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 로컬 포트 포워딩은 다음과 같이 설정해 사용 가능하다&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1691567626223&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -L [로컬에서 사용할 포트]:[최종적으로 접근할 곳(ip:port형태)] [SSH Server 주소]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예시로, aws에서 지급받은 pem키와 함께 사용하려면 다음과 같이 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1691568321483&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i [pem키 경로] -L 3306:[RDS 엔드포인트]:3306 [사용자명(ex:ubuntu)]@[Bastion Host public ip]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게하면 로컬의 3306포트와 Private RDS의 3306이 연결됐으니 이제 스프링부트에서도 datasource를 내 로컬의 3306으로 한 뒤 쓰면 된다. 물론 각각 보안그룹들이 다 설정돼있어야 한다!(RDS는 Bastion으로부터 3306포트로 오는 인바운드 허용, Bastion은 22번 인바운드와 3306아웃바운드 허용 등..) 이 때 Public Subnet에 둔 EC2는 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;점프호스트&lt;/b&gt;&lt;/span&gt;로 쓰는 거라고 볼 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만! SSM이란 서비스를 사용하면 좀 더 쉽게 Private Subnet접근이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;SSM이란?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333;&quot;&gt;AWS System Manager. Simple System Manager라고도 불리며 이의 줄임말이 SSM이다. 다양한 기능들을 제공하는데, &lt;span style=&quot;background-color: #fffffe; text-align: start;&quot;&gt;Session manager라는 걸 통해 EC2접속을 할 수 있는 서비스도 제공한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1280&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elZQcC/btsqKzaZyhG/PmPYlu07KxLoDbaabPNPUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elZQcC/btsqKzaZyhG/PmPYlu07KxLoDbaabPNPUK/img.png&quot; data-alt=&quot;https://aws.amazon.com/ko/blogs/architecture/how-wego-secured-developer-connectivity-to-amazon-relational-database-service-instances/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elZQcC/btsqKzaZyhG/PmPYlu07KxLoDbaabPNPUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FelZQcC%2FbtsqKzaZyhG%2FPmPYlu07KxLoDbaabPNPUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;381&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;381&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://aws.amazon.com/ko/blogs/architecture/how-wego-secured-developer-connectivity-to-amazon-relational-database-service-instances/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #fffffe; color: #555555; text-align: start;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691571721320&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;AWS Systems Manager&amp;nbsp;Session Manager - AWS Systems Manager&quot; data-og-description=&quot;AWS Systems Manager&amp;nbsp;Session Manager Session Manager is a fully managed AWS Systems Manager capability. With Session Manager, you can manage your Amazon Elastic Compute Cloud (Amazon EC2) instances, edge devices, on-premises servers, and virtual machines (&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&quot; data-og-url=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.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;AWS Systems Manager&amp;nbsp;Session Manager - AWS Systems Manager&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AWS Systems Manager&amp;nbsp;Session Manager Session Manager is a fully managed AWS Systems Manager capability. With Session Manager, you can manage your Amazon Elastic Compute Cloud (Amazon EC2) instances, edge devices, on-premises servers, and virtual machines (&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #fffffe; color: #555555; text-align: start;&quot;&gt;Session Manager를 사용하면 기존에 했던 것처럼 Bastion Host를 둘 필요 없이 Private Subnet에 있는 EC2인스턴스에 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;직접 접근이 가능&lt;/b&gt;&lt;/span&gt;하다(당연히 퍼블릭도 가능). ssh로 연결할 때 사용하던 pem키 관리 및 EC2인스턴스들에 22번 포트를 여는 인바운드 설정도 할 필요가 없다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;즉 더 안전하게 사용 가능한 방법&lt;/b&gt;&lt;/span&gt;이라고도 볼 수 있다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;또한 포트포워딩도 지원&lt;/b&gt;&lt;/span&gt;하기 때문에 로컬에서 Private Subnet에 있는 RDS로의 접근 역시 가능하다!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #fffffe; color: #555555; text-align: start;&quot;&gt;(Session Manager를 통해 EC2들에 대한 직접 액세스는 가능하나 RDS등은 직접 액세스는 안 돼서 포트포워딩을 거쳐야 함. 즉 EC2라는 관리 인스턴스를 &quot;점프 호스트&quot;로 사용해 RDS로 접근하는 것이며, ssh 포트포워딩을 통해 public에 있던 EC2를 점프호스트로 쓰던 것과 같은 원리이다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #fffffe; color: #555555; text-align: start;&quot;&gt;참고로 이때 SSM을 통해 직접 액세스하게 되는 EC2는 22번 인바운드는 열릴 필요가 없으나, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;443번 아웃바운드는 열려있어야 한다&lt;/b&gt;&lt;/span&gt;. 해당 EC2에서 AWS SSM에게 지속적인 폴링을 통해 연결을 유지하기 때문이다. 따라서 보안그룹 설정시 꼭 주의할 것. 관련된 내용은 아래 링크의&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; [엔드포인트에 연결]&lt;/b&gt; &lt;/span&gt;섹션을 참고하면 된다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #fffffe; color: #555555; text-align: start;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-prerequisites.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-prerequisites.html&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691572928598&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;1단계: Session Manager 사전 조건 완료 - AWS Systems Manager&quot; data-og-description=&quot;이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-prerequisites.html&quot; data-og-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-prerequisites.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-prerequisites.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-prerequisites.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;1단계: Session Manager 사전 조건 완료 - AWS Systems Manager&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼 본격적으로 SSM으로 포트포워딩을 설정해 로컬에서 Private Subnet의 RDS에 접속하는 법을 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 로컬환경에 aws cli 설치하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691587569476&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;최신 버전의&amp;nbsp;AWS CLI&amp;nbsp;설치 또는 업데이트 - AWS Command Line Interface&quot; data-og-description=&quot;이전 버전에서 업데이트하는 경우 unzip 명령을 실행하면 기존 파일을 덮어쓸지 묻는 메시지가 표시됩니다. 스크립트 자동화와 같은 경우에 이러한 프롬프트를 건너뛰려면 unzip에 대한 -u 업데이&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions&quot; data-og-url=&quot;https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions&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;최신 버전의&amp;nbsp;AWS CLI&amp;nbsp;설치 또는 업데이트 - AWS Command Line Interface&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전 버전에서 업데이트하는 경우 unzip 명령을 실행하면 기존 파일을 덮어쓸지 묻는 메시지가 표시됩니다. 스크립트 자동화와 같은 경우에 이러한 프롬프트를 건너뛰려면 unzip에 대한 -u 업데이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. aws IAM을 만든 다음, Access Key와 Secret Access Key를 발급받는다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 로컬에서 터미널을 띄우고 aws configure를 입력해 구성한다&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1691588114266&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ aws configure
AWS Access Key ID [None]: {각자에게 주어진 Access Key}
AWS Secret Access Key [None]: {각자에게 주어진 Secret Access Key}
Default region name [None]: ap-northeast-2
Default output format [None]: json&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 로컬에 Session Manager Plugin을 설치한다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691587880495&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;용 Session Manager 플러그인 설치 AWS CLI - AWS Systems Manager&quot; data-og-description=&quot;이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html&quot; data-og-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-working-with-install-plugin.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;용 Session Manager 플러그인 설치 AWS CLI - AWS Systems Manager&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5. EC2를 띄우고, 그 EC2에 AmazonSSMManagedInstanceCore라는 정책이 있는 역할을 부여한다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6VmTf/btsqKy4tLI2/ROJ4Xn0iEtlWFsQxsm7pTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6VmTf/btsqKy4tLI2/ROJ4Xn0iEtlWFsQxsm7pTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6VmTf/btsqKy4tLI2/ROJ4Xn0iEtlWFsQxsm7pTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6VmTf%2FbtsqKy4tLI2%2FROJ4Xn0iEtlWFsQxsm7pTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;594&quot; height=&quot;273&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 EC2에 SSM agent가 설치돼있어야 한다. aws에서 제공하는 AMI로 만들어진 EC2들은 ssm agent가 기본적으로 깔려있는 애들이 많다! 다음 링크의 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[SSM Agent의 상태 확인]&lt;/b&gt;&lt;/span&gt; 섹션을 참고해 SSM agent가 설치돼있는지 살펴보고, 안돼있으면 설치해야한다. 설치된 상황이라면 해당 역할을 부여한 후 SSM agent를 재시작해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;재시작하는 커맨드는 상태 확인 커맨드에서 status만 restart로 바꾸면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ami-preinstalled-agent.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ami-preinstalled-agent.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691588210233&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;Amazon Machine Images(AMIs), SSM Agent 사전 설치 - AWS Systems Manager&quot; data-og-description=&quot;SSM Agent가 이 목록에 없는 AWS 관리형 AMIs에 사전 설치되어 있을 수 있습니다. 이 경우 일반적으로 운영 체제(OS)에서 모든 Systems Manager 기능이 완벽하게 지원되지 않습니다. 또는 SSM Agent가 AWS Marketp&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ami-preinstalled-agent.html&quot; data-og-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ami-preinstalled-agent.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ami-preinstalled-agent.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ami-preinstalled-agent.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;Amazon Machine Images(AMIs), SSM Agent 사전 설치 - AWS Systems Manager&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SSM Agent가 이 목록에 없는 AWS 관리형 AMIs에 사전 설치되어 있을 수 있습니다. 이 경우 일반적으로 운영 체제(OS)에서 모든 Systems Manager 기능이 완벽하게 지원되지 않습니다. 또는 SSM Agent가 AWS Marketp&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;6. 터미널에 다음 커맨드를 입력한다&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1691588350235&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aws ssm start-session --target {EC2 인스턴스의 id} \
                       --document-name AWS-StartPortForwardingSessionToRemoteHost \
                       --parameters '{&quot;portNumber&quot;:[&quot;3306&quot;],&quot;localPortNumber&quot;:[&quot;3306&quot;],&quot;host&quot;:[&quot;{RDS 엔드포인트}&quot;]}'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러면 ssh 포트포워딩을 했을 때처럼 로컬의 3306포트와 RDS의 3306포트가 연결된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;참고한 글&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://blog.naver.com/PostView.naver?blogId=alice_k106&amp;amp;logNo=221364560794&quot;&gt;https://blog.naver.com/PostView.naver?blogId=alice_k106&amp;amp;logNo=221364560794&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691566789828&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;150. [SSH, Network] SSH 포트 포워딩(SSH 터널링)의 개념 및 사용 방법&quot; data-og-description=&quot;이번 포스트에서는 'SSH 포트 포워딩' 또는 'SSH 터널링' 이라고 불리는 것에 대해서 설명한다. 1. ...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/PostView.naver?blogId=alice_k106&amp;amp;logNo=221364560794&quot; data-og-url=&quot;https://blog.naver.com/alice_k106/221364560794&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VVDCn/hyTBF8ldFC/poW5DRVuAJTV804tRO0Vz1/img.png?width=740&amp;amp;height=376&amp;amp;face=0_0_740_376&quot;&gt;&lt;a href=&quot;https://blog.naver.com/PostView.naver?blogId=alice_k106&amp;amp;logNo=221364560794&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.naver.com/PostView.naver?blogId=alice_k106&amp;amp;logNo=221364560794&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VVDCn/hyTBF8ldFC/poW5DRVuAJTV804tRO0Vz1/img.png?width=740&amp;amp;height=376&amp;amp;face=0_0_740_376');&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;150. [SSH, Network] SSH 포트 포워딩(SSH 터널링)의 개념 및 사용 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 'SSH 포트 포워딩' 또는 'SSH 터널링' 이라고 불리는 것에 대해서 설명한다. 1. ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://musma.github.io/2019/11/29/about-aws-ssm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://musma.github.io/2019/11/29/about-aws-ssm.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691566797447&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;AWS SSM으로 EC2 인스턴스에 접근하기 (SSH 대체)&quot; data-og-description=&quot;목차 서론 들어가기: 더 좋은 방법 대상 독자 SSM: AWS Systems Manager 원격 호스트 접속 방법 비교: SSH (기존) vs. SSM (개선) S...&quot; data-og-host=&quot;musma.github.io&quot; data-og-source-url=&quot;https://musma.github.io/2019/11/29/about-aws-ssm.html&quot; data-og-url=&quot;https://musma.github.io/2019/11/29/about-aws-ssm.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cHX9yJ/hyTBJXdBSY/WcwcTV9v5iaDmKtDEhdWRK/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/eiRiCJ/hyTBu6Puk9/Gv7U821jAEKfTzjhkeYXqK/img.png?width=532&amp;amp;height=138&amp;amp;face=0_0_532_138,https://scrap.kakaocdn.net/dn/sJhHK/hyTBJbQAHc/Owdfi9ZSPi7e2WeeWUN5JK/img.png?width=532&amp;amp;height=138&amp;amp;face=0_0_532_138&quot;&gt;&lt;a href=&quot;https://musma.github.io/2019/11/29/about-aws-ssm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://musma.github.io/2019/11/29/about-aws-ssm.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cHX9yJ/hyTBJXdBSY/WcwcTV9v5iaDmKtDEhdWRK/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/eiRiCJ/hyTBu6Puk9/Gv7U821jAEKfTzjhkeYXqK/img.png?width=532&amp;amp;height=138&amp;amp;face=0_0_532_138,https://scrap.kakaocdn.net/dn/sJhHK/hyTBJbQAHc/Owdfi9ZSPi7e2WeeWUN5JK/img.png?width=532&amp;amp;height=138&amp;amp;face=0_0_532_138');&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;AWS SSM으로 EC2 인스턴스에 접근하기 (SSH 대체)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목차 서론 들어가기: 더 좋은 방법 대상 독자 SSM: AWS Systems Manager 원격 호스트 접속 방법 비교: SSH (기존) vs. SSM (개선) S...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;musma.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hbase.tistory.com/328&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hbase.tistory.com/328&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691567085376&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;[Linux] ssh 터널링(ssh port forwarding) - Local / Remote / Dynamic Tunneling&quot; data-og-description=&quot;sh는 Secure SHell의 줄임말로 원격 호스트에 접속하기 위해 사용되는 보안 프로토콜이다. 당연하게도 ssh는 원격 호스트로 접속하기 위해 가장 많이 사용된다. 그런데 ssh는 원격 호스트로의 접속과 &quot; data-og-host=&quot;hbase.tistory.com&quot; data-og-source-url=&quot;https://hbase.tistory.com/328&quot; data-og-url=&quot;https://hbase.tistory.com/328&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/DdUt2/hyTBEayjOF/bAYMuQMnBMg4w3cT0hPvp1/img.png?width=800&amp;amp;height=232&amp;amp;face=0_0_800_232,https://scrap.kakaocdn.net/dn/clP9Sj/hyTBEBCvgV/v5r8EqDwF7kKCg4mlVKFV1/img.png?width=800&amp;amp;height=232&amp;amp;face=0_0_800_232,https://scrap.kakaocdn.net/dn/bjZSRW/hyTByBpoGH/lXH4B8PRrFh5YfN8nWcVXK/img.png?width=1245&amp;amp;height=479&amp;amp;face=0_0_1245_479&quot;&gt;&lt;a href=&quot;https://hbase.tistory.com/328&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hbase.tistory.com/328&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/DdUt2/hyTBEayjOF/bAYMuQMnBMg4w3cT0hPvp1/img.png?width=800&amp;amp;height=232&amp;amp;face=0_0_800_232,https://scrap.kakaocdn.net/dn/clP9Sj/hyTBEBCvgV/v5r8EqDwF7kKCg4mlVKFV1/img.png?width=800&amp;amp;height=232&amp;amp;face=0_0_800_232,https://scrap.kakaocdn.net/dn/bjZSRW/hyTByBpoGH/lXH4B8PRrFh5YfN8nWcVXK/img.png?width=1245&amp;amp;height=479&amp;amp;face=0_0_1245_479');&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;[Linux] ssh 터널링(ssh port forwarding) - Local / Remote / Dynamic Tunneling&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;sh는 Secure SHell의 줄임말로 원격 호스트에 접속하기 위해 사용되는 보안 프로토콜이다. 당연하게도 ssh는 원격 호스트로 접속하기 위해 가장 많이 사용된다. 그런데 ssh는 원격 호스트로의 접속과&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hbase.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hwan-shell.tistory.com/382&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hwan-shell.tistory.com/382&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691571250497&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;AWS Session manager 란? (설명 및 설정 방&quot; data-og-description=&quot;EC2를 접속할 때 기본적으로 SSH를 통해서 접속합니다. SSH를 통해서 접속하려면 SSH key가 필요하고 EC2 Inbound 22 port를 허용해 줘야 합니다. SSH를 사용하게 되면 Key 없이는 접속할 수 없고, Key를 분실&quot; data-og-host=&quot;hwan-shell.tistory.com&quot; data-og-source-url=&quot;https://hwan-shell.tistory.com/382&quot; data-og-url=&quot;https://hwan-shell.tistory.com/382&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oUYSo/hyTBw4M4NG/ztleKP2WkMY7HEWCDk1Nt0/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/bG8HYx/hyTByVRLcD/EHXwM2UAaKwWv3PaM4LpU0/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/x2WzE/hyTBCRsSCk/abjsX3O22MBNhQS9qPwGfk/img.png?width=1596&amp;amp;height=480&amp;amp;face=0_0_1596_480&quot;&gt;&lt;a href=&quot;https://hwan-shell.tistory.com/382&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hwan-shell.tistory.com/382&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oUYSo/hyTBw4M4NG/ztleKP2WkMY7HEWCDk1Nt0/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/bG8HYx/hyTByVRLcD/EHXwM2UAaKwWv3PaM4LpU0/img.png?width=800&amp;amp;height=240&amp;amp;face=0_0_800_240,https://scrap.kakaocdn.net/dn/x2WzE/hyTBCRsSCk/abjsX3O22MBNhQS9qPwGfk/img.png?width=1596&amp;amp;height=480&amp;amp;face=0_0_1596_480');&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;AWS Session manager 란? (설명 및 설정 방&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;EC2를 접속할 때 기본적으로 SSH를 통해서 접속합니다. SSH를 통해서 접속하려면 SSH key가 필요하고 EC2 Inbound 22 port를 허용해 줘야 합니다. SSH를 사용하게 되면 Key 없이는 접속할 수 없고, Key를 분실&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hwan-shell.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/about-aws/whats-new/2022/05/aws-systems-manager-support-port-forwarding-remote-hosts-using-session-manager/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://aws.amazon.com/ko/about-aws/whats-new/2022/05/aws-systems-manager-support-port-forwarding-remote-hosts-using-session-manager/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691572283165&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;company&quot; data-og-title=&quot;AWS Systems Manager, Session Manager를 사용한 원격 호스트로의 포트 포워딩 지원 발표&quot; data-og-description=&quot;AWS Systems Manager가 Session Manager를 사용한 원격 호스트로의 포트 포워딩 지원을 발표합니다. AWS Systems Manager는 AWS 애플리케이션 및 리소스를 위한 운영 허브이며, 하이브리드 클라우드 환경에 대해 &quot; data-og-host=&quot;aws.amazon.com&quot; data-og-source-url=&quot;https://aws.amazon.com/ko/about-aws/whats-new/2022/05/aws-systems-manager-support-port-forwarding-remote-hosts-using-session-manager/&quot; data-og-url=&quot;https://aws.amazon.com/ko/about-aws/whats-new/2022/05/aws-systems-manager-support-port-forwarding-remote-hosts-using-session-manager/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dvkxBA/hyTBzAuOqk/eyuGQQUNJ8CTPy27IhVUm0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/5fZfU/hyTBuzazlB/wulH5DqwsIkUZPhP8sSow0/img.png?width=179&amp;amp;height=109&amp;amp;face=0_0_179_109&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/about-aws/whats-new/2022/05/aws-systems-manager-support-port-forwarding-remote-hosts-using-session-manager/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aws.amazon.com/ko/about-aws/whats-new/2022/05/aws-systems-manager-support-port-forwarding-remote-hosts-using-session-manager/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dvkxBA/hyTBzAuOqk/eyuGQQUNJ8CTPy27IhVUm0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/5fZfU/hyTBuzazlB/wulH5DqwsIkUZPhP8sSow0/img.png?width=179&amp;amp;height=109&amp;amp;face=0_0_179_109');&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;AWS Systems Manager, Session Manager를 사용한 원격 호스트로의 포트 포워딩 지원 발표&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AWS Systems Manager가 Session Manager를 사용한 원격 호스트로의 포트 포워딩 지원을 발표합니다. AWS Systems Manager는 AWS 애플리케이션 및 리소스를 위한 운영 허브이며, 하이브리드 클라우드 환경에 대해&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;</description>
      <category>INFRA &amp;amp; DEVOPS/AWS</category>
      <category>EC2</category>
      <category>Private Subnet</category>
      <category>RDS</category>
      <category>SSH</category>
      <category>SSM</category>
      <category>로컬</category>
      <category>연결</category>
      <category>포트포워드</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/110</guid>
      <comments>https://jofestudio.tistory.com/110#entry110comment</comments>
      <pubDate>Wed, 9 Aug 2023 22:42:31 +0900</pubDate>
    </item>
    <item>
      <title>lombok - is로 시작하는 boolean타입에 대해 만들어지는 Getter에 대해</title>
      <link>https://jofestudio.tistory.com/109</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트를 진행하면서, 개의 중성화여부를 나타내는 컬럼을 추가해달라는 요청을 받았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;true / false값을 저장하는 is_neutered필드를 추가하기로 했고, 찾아보니 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MySQL에서는 boolean타입을 별도로 지원하지 않는다고 해서 TINYINT(1)타입으로 필드를 만들어줬다. 1은 true, 0은 false를 나타낼 수 있게끔.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;1482&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SLpFL/btsqB9DKAAQ/KcwAks9BwkqvtWT6QCYl9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SLpFL/btsqB9DKAAQ/KcwAks9BwkqvtWT6QCYl9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SLpFL/btsqB9DKAAQ/KcwAks9BwkqvtWT6QCYl9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSLpFL%2FbtsqB9DKAAQ%2FKcwAks9BwkqvtWT6QCYl9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;210&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691484206758&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Dog entity
    @Column(columnDefinition = &quot;TINYINT(1)&quot;, nullable = false)
    private boolean isNeutered;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 강아지를 등록할 때 사용하는 dto들에도 마찬가지로 isNeutuerd라는 필드들을 추가해줬다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691484323546&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class SignupRequestDto {
    private String email;
    private String name;
    private String dogName;
    private String dogType;
    private String dogGender;
    private boolean isNeutered;
    private int dogAge;
    
    // ... 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 이윽고 잠시 후, 문제가 생겼다. 실제 DB에 쌓이는 데이터들을 보는데, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;dog테이블의 is_neutered컬럼이 죄다 0만 저장되고 있던 거&lt;/b&gt;&lt;/span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 프론트(플러터 단)에서는 요로코롬 true값을 담아서 request를 보내고 있던 상황이었음&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691484702464&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      var res = await dio.post(
        &quot;http://localhost:8080/api/auth/signup?provider=$oauth2Provider&quot;,
        data: {
          &quot;email&quot;: email,
          &quot;name&quot;: _userName,
          &quot;dog_name&quot;: _dogName,
          &quot;dog_type&quot;: _dogType,
          &quot;dog_gender&quot;: _dogGender,
          &quot;is_neutered&quot;: true,
          &quot;dog_age&quot;: _dogAge,
        },
      );&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;뭘까 싶어서 컨트롤러에서 SignupRequestDto의 값을 찍어봤는데, 이럴 수가!! (ㅎㅎ..내가 봐도 오글거리는 말투다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;isNeutered필드 값이 false가 찍히고 있었다!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;단순히 이 현상만 보자면 프론트에서는 true를 넘겼는데 백엔드에서 받고 보니까 false다..? 중간에 해커가 탈취해서 조작했나..? 오만가지 생각이 다 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 구글링을 통해 비교적 쉽게 문제를 해결할 수 있었다. 바로 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;dto에서 boolean이 아니라 Boolean으로 타입을 변경해주는 것&lt;/b&gt;&lt;/span&gt;을 통해서!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일단 원인부터 정리하자면 다음과 같았다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;롬복(Lombok)은 @Getter나 @Setter를 붙이면 필드들에 대해 get뭐시기, set뭐시기같은 게터 &amp;amp; 세터 메소드들을 만들어준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 만약 필드가 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;boolean타입이면서 is로 시작하는 필&lt;/b&gt;&lt;b&gt;드&lt;/b&gt;&lt;/span&gt;라면(ex: isExpired), getIsExpired나 setIsExpired같은 이름으로 메서드를 만들어주는게 아니라 isExpired, setExpired란 이름의 게터와 세터를 만들어준다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 내가 SignupRequestDto에 작성한 isNeutered라는 필드에 대한 게터가 getIsNeutered가 아닌 isNeutered로 작성된 것. 근데 이게 왜 문제가 되는가? 바로 컨트롤러에 있는&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; @RequestBody&lt;/b&gt;&lt;/span&gt;에서 문제가 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691489116308&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @PostMapping(&quot;/signup&quot;)
    public BaseResponse&amp;lt;JwtResponseDto&amp;gt; signup(@RequestParam(&quot;provider&quot;) String oauth2ProviderName,
                                               @RequestBody SignupRequestDto signupRequestDto) {
        Oauth2Provider oauth2Provider =
                Oauth2Provider.getOauth2ProviderByName(oauth2ProviderName);
        
        // ... 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;@RequestBody가 어떻게 동작하길래 dto의 게터가 isNeutered인게 문제가 되는 걸까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;알다시피 이 놈은 client가 보낸 request의 json데이터를 Dto에 매핑해주는 역할(역직렬화)을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MappingJackson2HttpMessageConverter가 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;내부적으로 ObjectMapper를 활용해서 값을 dto에 바인딩&lt;/b&gt;&lt;/span&gt;해주는데, 이 때 dto에 게터들이 있어야 한다. 즉 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;get뭐시기라는 형식의 이름을 가진 메서드들이 있어야 하는데, 롬복선생께서 isNeutered의 게터를 getIsNeutered가 아니라 isNeutered로 만들어줘서 문제&lt;/b&gt;&lt;/span&gt;가 생기는 것. (적절한 게터를 찾게 되지 못하는 것이기 때문) 이때 boolean타입의 기본값은 false기 때문에 자동으로 false가 들어가져있던 거다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 이를 해결하기 위해선 이리 볶든 저리 볶든 Dto클래스에 getIsNeutered라는 게터가 있으면 되는거다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 내가 직접 게터를 dto클래스에 만들어주면 되나! &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;필드의 타입을 boolean이 아닌 Boolean으로 작성(즉 래퍼클래스를 쓰는 형태로 작성)하면, is라는 접두사로 시작하는 필드여도 get뭐시기 라는 형태로 만들어진다&lt;/b&gt;&lt;/span&gt;고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691490807283&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class SignupRequestDto {
    private String email;
    private String name;
    private String dogName;
    private String dogType;
    private String dogGender;
    private Boolean isNeutered;
    private int dogAge;
    
    // ... 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;✅ 참고한 글들&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691485430414&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;blog&quot; data-og-title=&quot;RequestBody Annotation 사용 시 boolean 변수 바인딩 에러&quot; data-og-description=&quot;서론토이프로젝트 중 @Reqeust 어노테이션을 적용한 DTO에서 boolean 데이터를 제대로 전달 받지 못하는 문제가 발생하여 이를 정리한다. 문제 발생vue.js에서 넘어온 데이터를 @RequestBody 어노테이션을&quot; data-og-host=&quot;jungguji.github.io&quot; data-og-source-url=&quot;https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/&quot; data-og-url=&quot;https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/index.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CCKw2/hyTAueNjQS/1Z7hd9RcluCJadlcyMs4q0/img.png?width=489&amp;amp;height=268&amp;amp;face=0_0_489_268&quot;&gt;&lt;a href=&quot;https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CCKw2/hyTAueNjQS/1Z7hd9RcluCJadlcyMs4q0/img.png?width=489&amp;amp;height=268&amp;amp;face=0_0_489_268');&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;RequestBody Annotation 사용 시 boolean 변수 바인딩 에러&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론토이프로젝트 중 @Reqeust 어노테이션을 적용한 DTO에서 boolean 데이터를 제대로 전달 받지 못하는 문제가 발생하여 이를 정리한다. 문제 발생vue.js에서 넘어온 데이터를 @RequestBody 어노테이션을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jungguji.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://sedangdang.tistory.com/305&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://sedangdang.tistory.com/305&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691491033557&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;@RequestBody 공식문서를 읽어보자 + HttpMessageConverter&quot; data-og-description=&quot;의역/오역이 있을 수 있고 잘못된 부분이 존재할 수 있습니다 요약 1. @RequestBody는 HttpMessageConverter가 HTTP Request Body 내의 데이터를 객체로 변환(역직렬화)하도록 시키는 애노테이션이다. 2. @Valid를 &quot; data-og-host=&quot;sedangdang.tistory.com&quot; data-og-source-url=&quot;https://sedangdang.tistory.com/305&quot; data-og-url=&quot;https://sedangdang.tistory.com/305&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b0AUpd/hyTAy9rQV3/KVTPD8KaZ6pyh5sFgLkXjK/img.png?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/bCOKGq/hyTAF1OKzC/8khfJYgLTJLW18YbLHjg21/img.png?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/zezXu/hyTAsuEPn9/I4hcGKVFe9MfZzrwjH0lDK/img.png?width=1109&amp;amp;height=376&amp;amp;face=0_0_1109_376&quot;&gt;&lt;a href=&quot;https://sedangdang.tistory.com/305&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sedangdang.tistory.com/305&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b0AUpd/hyTAy9rQV3/KVTPD8KaZ6pyh5sFgLkXjK/img.png?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/bCOKGq/hyTAF1OKzC/8khfJYgLTJLW18YbLHjg21/img.png?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/zezXu/hyTAsuEPn9/I4hcGKVFe9MfZzrwjH0lDK/img.png?width=1109&amp;amp;height=376&amp;amp;face=0_0_1109_376');&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;@RequestBody 공식문서를 읽어보자 + HttpMessageConverter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;의역/오역이 있을 수 있고 잘못된 부분이 존재할 수 있습니다 요약 1. @RequestBody는 HttpMessageConverter가 HTTP Request Body 내의 데이터를 객체로 변환(역직렬화)하도록 시키는 애노테이션이다. 2. @Valid를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sedangdang.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://velog.io/@soyeon207/Spring-Lombok-%EC%82%AC%EC%9A%A9%EC%8B%9C-is-prefix-%EB%8A%94-%EC%99%9C-%EC%97%86%EC%96%B4%EC%A7%88%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@soyeon207/Spring-Lombok-%EC%82%AC%EC%9A%A9%EC%8B%9C-is-prefix-%EB%8A%94-%EC%99%9C-%EC%97%86%EC%96%B4%EC%A7%88%EA%B9%8C&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1691491066422&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;Lombok 사용시 is prefix  는 왜 없어질까 ?&quot; data-og-description=&quot;isNew 라는 신상품 여부를 나타내는 변수가 있었는데 response 로 DTO 내려줄 때 is 가 사라지고 new 라고만 계속 보여졌다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@soyeon207/Spring-Lombok-%EC%82%AC%EC%9A%A9%EC%8B%9C-is-prefix-%EB%8A%94-%EC%99%9C-%EC%97%86%EC%96%B4%EC%A7%88%EA%B9%8C&quot; data-og-url=&quot;https://velog.io/@soyeon207/Spring-Lombok-사용시-is-prefix-는-왜-없어질까&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vDn5M/hyTAww2QwV/MhZlB7mbBOnUmbGITY4nW1/img.png?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/0ysVo/hyTAt1m5vu/mvlsjufDaK0vKRkNG7jo70/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://velog.io/@soyeon207/Spring-Lombok-%EC%82%AC%EC%9A%A9%EC%8B%9C-is-prefix-%EB%8A%94-%EC%99%9C-%EC%97%86%EC%96%B4%EC%A7%88%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@soyeon207/Spring-Lombok-%EC%82%AC%EC%9A%A9%EC%8B%9C-is-prefix-%EB%8A%94-%EC%99%9C-%EC%97%86%EC%96%B4%EC%A7%88%EA%B9%8C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vDn5M/hyTAww2QwV/MhZlB7mbBOnUmbGITY4nW1/img.png?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/0ysVo/hyTAt1m5vu/mvlsjufDaK0vKRkNG7jo70/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;Lombok 사용시 is prefix  는 왜 없어질까 ?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;isNew 라는 신상품 여부를 나타내는 변수가 있었는데 response 로 DTO 내려줄 때 is 가 사라지고 new 라고만 계속 보여졌다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>WEB/Spring</category>
      <category>boolean</category>
      <category>is</category>
      <category>requestbody</category>
      <category>롬복</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/109</guid>
      <comments>https://jofestudio.tistory.com/109#entry109comment</comments>
      <pubDate>Tue, 8 Aug 2023 19:39:13 +0900</pubDate>
    </item>
    <item>
      <title>[짧] Hard delete vs Soft delete</title>
      <link>https://jofestudio.tistory.com/108</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;둘 다 DB에서 데이터를 삭제하는 방식에 관한 것이다. &lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;Hard delete는 물리 삭제라고도 부르고 Soft delete는 논리 삭제라고도 부르는데, &lt;/span&gt;이름에서 유추할 수 있듯이 Hard delete는 &lt;b&gt;물리적으로 데이터 자체를 DB에서 없애는 것&lt;/b&gt;이고 Soft delete는 &lt;b&gt;논리적인 범주에서만 없애는 것&lt;/b&gt;이다. 즉 Soft delete는 데이터 자체가 물리적으론 DB에 남아있다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;논리적으로 삭제한다는 것 = Soft하게 삭제한다는 것이 어떤 의미일까? &lt;b&gt;삭제한 것처럼 여기겠다&lt;/b&gt; 라는 것이다. 테이블에 삭제여부를 나타내는 컬럼을 추가해 삭제여부가 표기돼있으면 삭제됐다고 보고, 삭제여부가 표기돼있지 않으면 삭제가 안 된 데이터로 본다는 것,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;이름&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;삭제여부&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;권은비&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;false&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;카리나&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;true&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그럼 이 Soft delete라는 것을 적용할 때 얻을 수 있는 장점은 뭘까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;당연히 &lt;b&gt;뒷배가 있다는게 최고의 장점&lt;/b&gt;이다. 삭제처리된 데이터들을 따로 보고 싶을 때, 논리적으론 삭제됐지만 물리적으로는 데이터가 남아있는 셈이니 삭제된 데이터들을 따로 볼 때도 유용할 것이고, 실수로 인한 삭제가 발생했을 시 복원도 가능하다. 또한 물리적 삭제의 경우 delete라는 SQL을 쓰고 논리적 삭제는 update라는 SQL을 쓰는데, update가 delete에 비해 좀 더 빠른 속도를 낸다고도 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;하지만 Soft delete는 단점도 있다. 우선 물리적으로 데이터를 하드디스크에서 덜어내는 게 아니기 때문에 시간이 지날수록 물리적으로 쌓여있는 데이터들이 많아지며 DB 용량 자체가 커진다. 또 다른 단점은 where절 등으로 데이터들을 뽑아낼 때 삭제여부에 대한 처리도 반드시 함께 해줘야 하는 것. 이는 쿼리문을 직관적으로 이해하는 것에도 불편을 줄 가능성이 존재한다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;따라서 이런 장단점들을 비교해보면서, 서비스 운영에 맞게 Soft delete를 사용할지 Hard delete를 사용할지 잘 고려해야 할 필요가 있을 것이다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>DB</category>
      <category>DB</category>
      <category>hard delete</category>
      <category>soft delete</category>
      <category>논리삭제</category>
      <category>물리삭제</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/108</guid>
      <comments>https://jofestudio.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 6 Aug 2023 20:16:47 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] IAM이란? (사용자, 정책, 그룹, 역할, 정책 평가 방법 등등)</title>
      <link>https://jofestudio.tistory.com/107</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;윈도우가 깔린 컴퓨터를 켜면, 처음에 어떤 유저로 들어갈 건지 고를 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;리눅스도 마찬가지다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각각의 유저별로, 하나의 컴퓨터에 대해 할 수 있는 동작들이 다르다. 누구는 A를 할 수 있지만 누구는 할 수 없고 등등..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AWS도 이와 비슷하게 &quot;유저들&quot;이라는 개념이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;조직원들이 쓰고 있는 전체 자원들에 대해, 누구는 A를 할 수 있지만 누구는 못 하게끔 설정이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;AWS에서, &quot;계정&quot;과 &quot;사용자&quot;는 다른 개념&lt;/b&gt;&lt;/span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;계정은 root, 즉 리눅스에서의 super user같은 개념이다. 과금의 주체가 되는 주인이라고 보면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자는 이용자, 즉 root가 허가한 권한만 갖는 애들이라고 보면 된다. 위에서 방금 말했던 &quot;유저들&quot;이란 개념을 상기하자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AWS계정을 처음 만들면 root 사용자로 시작하는 것이며, 모든 AWS서비스 및 리소스에 대한 전체 액세스 권한을 갖는다(모든 걸 다 할 수 있다는 의미).&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 후 root가 유저들을 만들면서 이 유저들한테 특정 권한들(어떤 리소스에 대해 어떤 동작을 허가한다와 같은)을 줄 수 있는 거다. 이것이 사용자라는 개념이라고 이해하면 된다. 관리자 유저한테는 ~~권한들을 주고, 개발자 유저한테는 ~~권한들을 주고, 감사자 유저한테는 ~~권한들을 주고 그런 식으로 세팅할 수 있는 것.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;암튼.. 이런 것들에 대해 적용되는 개념이 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;IAM = Identity and Access Management&lt;/b&gt;&lt;/span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;IAM이라는 서비스와 구성요소&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;IAM은 한 줄로 말하자면 &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;AWS에서 사용자를 만들고 권한을 주는 것을 통해 AWS 리소스에 대한 액세스를 안전하게 제어할 수 있게끔 하는 서비스&lt;/b&gt;&lt;/span&gt;이다. 각 유저들에 대한 권한 제어는 policy(=정책)라는 json포맷의 텍스트(쉽게 말하면 &quot;어떤 걸 할 수 있다&quot;들의 목록같은 거다)를 통해 이뤄지며, IAM은 다음과 같은 4개로 구성된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;사용자 : 위에서 말한 개념&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그룹 : 각 사용자들을 그룹핑 가능하며, 그룹에 정책을 지정하면 그룹원들이 동일한 권한들을 갖게 할 수 있다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정책&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;먼저 &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;정책&lt;/b&gt;&lt;/span&gt;에 대해 얘기해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다시 한 번 말하자면 &quot;어떤 걸 할 수 있다&quot;라는 것들..즉 &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&quot;&lt;b&gt;권한들의 목록&quot;&lt;/b&gt;&lt;/span&gt;이 정책이다 라고 보면 되며, 사용자들은 이렇게 연결된 정책에서 지정된 권한들만 쓸 수 있다.. 그렇기 때문에 사용자를 만들고 그룹을 배정해주든 정책을 연결해주든 아무것도 안 해준다면 사실상 그 놈은 아무고토 할 수가 없다. 왜냐하먄 어떠한 권한도 없는 셈이니까. 리소스 기반 정책과 자격증명 기반 정책으로 분류할 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;리소스 기반 정책 : &lt;span style=&quot;background-color: #f6e199; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&quot;리소스&quot;에 연결되는 정책&lt;/b&gt;&lt;/span&gt;. 예를 들면 S3 bucket에는 ~~만 액세스할 수 있다! 처럼&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자격 증명 기반 정책 : &quot;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;사용자나 그룹 또는 역할&quot;에게 권한을 부여하는 식으로 사용하는 정책&lt;/span&gt;&lt;/b&gt;.&lt;/span&gt; 예를 들면 넌 EC2에 대해서는 읽기만 가능해! 등등&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;자격 증명 기반 정책&lt;/b&gt;&lt;/span&gt;은 다음과 같이 두 종류로 분류할 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;Inline Policy : 1 : 1로 사용되는 정책. 적용된 사용자가 없어지면 이 정책도 사라진다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Managed Policy : 1 : N으로 사용되는 정책. 즉 정책을 object화해서 다른 애들한테도 이 정책을 멕일 수 있다. 적용된 사용자가 사라져도 이 정책은 남아있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;Managed Policy&lt;/b&gt;&lt;/span&gt;는 다음과 같이 두 종류로 분류할 수 있다. 이번이 마지막임ㅋㅋㅋㅋ&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; 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;font-family: 'Noto Sans Light';&quot;&gt;AWS managed Policy : AWS에서 미리 만들어둔 애들. 변경할 수 없다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Customer managed Policy : 사용자가 생성한 정책!&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 Customer managed Policy를 만들 땐 Policy generator라는 걸 쓰는게 좋다. 하나하나 타이핑해서 json파일을 만드는 것보단 프로그램을 통해 편하게 만들 수 있기 때문.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(또한 이렇게 정책들을 다 멕인 후에, simulator를 통해서 이런저런 정책들이 적용된 상황에서의 테스트가 가능하다)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 다음으론, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/span&gt;에 대해 얘기해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;얘는 쉽게 말해서, 임시로 권한들을 부여하는 기능(즉 당일 회수되는 임시 출입증을 발급하는..)이라고 보면 된다. 내가 원래 다른 권한들을 가지고 있어도, &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;특정 역할을 잠깐 맡는다면 그 역할에 부여된 정책들을 임시로 사용할 수 있는 거&lt;/b&gt;&lt;/span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주의할 점은 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;병합의 개념이 아니기 때문&lt;/b&gt;&lt;/span&gt;에, 기존에 정책들을 가지고 있던 사용자가 역할을 받으면 원래 가지고 있던 정책들은 가려진다. &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;즉 역할을 통해 임시로 받는 권한들만 사용 가능&lt;/b&gt;&lt;/span&gt;해진다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로, AWS lambda와 같은 AWS 서비스들도 역할이란 걸 받을 수 있으니 알아두면 좋다:)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;IAM에서 정책을 평가하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼 IAM에서 정책은 어떤 순서대로 적용될까? 한마디로 사용자에게 부여된 권한들은 어떤 순서로 평가돼서 특정 리소스들에 대해 얘는 거부되는 거고, 얘는 승인하는건지 평가되는지 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로, AWS는 어떤 권한에 대해 명시적으로 언급하지 않았다면 그 권한에 대해 암묵적으로 Deny, 즉 거부가 된다. 명시적으로 Allow, 즉 승인을 해줘야만 그 권한을 이용가능하다고 생각하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;암튼 평가 순서는&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선, 그 권한에서 이야기하는 작업이 명시적으로 거부되는지를 본다. 만약 명시적으로 거부했다면 더 볼 필요도 없이 해당 권한이 없는 걸로 결론짓고 제낀다. 명시적으로 거부되지 않았다면, 다음 스텝으로 넘어간다&lt;/li&gt;
&lt;li&gt;그럼 그 작업이 명시적으로 승인되는지를 본다. 만약 명시적으로 승인됐다면 그 권한이 있는 걸로 결론지어진다. 만약 그렇지 않다? 이는 그 권한에 대해 아무런 명시도 안 한 것이니 암묵적으로 거부했다고 판단한다. 즉 해당 권한이 없는 것으로 결론짓는다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;뭐 이런 식으로 해당 권한이 있고 없고가 결정된다고 보면 된다. 이렇게 평가된 정책들에 대해, 사용자에게 부여된 자격 증명 기반 정책들을 통과하고, 리소스들에 부여된 리소스 기반 정책(VPC 엔드포인트 정책, S3 버킷 정책 등등)을 통과해야 비로소 해당 리소스에 접근할 수 있게 되는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;IAM 권한 경계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자격 증명 기반 정책들이 IAM 사용자들에게 부여할 수 있는 최대 권한을 설정하는 기능이다. 즉 &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;나한테 부여된 정책들 중 내가 쓸 수 있는 정책들을 제한해주는 기능&lt;/b&gt;&lt;/span&gt;이라고 보면 된다. 따라서 권한 경계와 나에게 부여된 자격 증명 기반 정책의 교집합이 내가 쓸 수 있는 정책이 될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;AWS orginizations&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다중 계정을 관리하는 것을 말한다. 사용자들을 그룹핑해서 그룹을 만들어주던 것을 계정에도 그대로 적용해서, 계정들도 조직을 만들어 그룹핑함으로써 특정 조직은 어떤 걸 할 수 있게 하고, 특정 조직은 어떤 걸 못 하게 하고 이런 걸 설정할 수가 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 조직에 허가되는 정책을 SCP(Service Control Polity)라고 한다. 즉.. 사용자는 자신이 속한 계정이 속한 organization에서 허가된 권한들만 쓸 수 있는 거..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리하면, 내가 만약 AWS에서 뭔가 작업하고 싶다면&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일단 내가 속한 계정의 조직이 이 작업을 허용했는지 봐야 하고&lt;/li&gt;
&lt;li&gt;적용된 IAM 권한 경계를 따져서 내가 할 수 있는 건지 봐야하고&lt;/li&gt;
&lt;li&gt;내게 부여된 자격 증명 기반 정책이 이 작업을 허용하고 있는지&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;를 따지게 되는 거다.. 이렇게 보니 꽤나 복잡한 듯&lt;/span&gt;&lt;/p&gt;</description>
      <category>INFRA &amp;amp; DEVOPS/AWS</category>
      <category>AWS</category>
      <category>IAM</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/107</guid>
      <comments>https://jofestudio.tistory.com/107#entry107comment</comments>
      <pubDate>Fri, 21 Jul 2023 12:39:26 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] SocketException: Connection refused (OS Error: Connection refused, errno = 111) 해결</title>
      <link>https://jofestudio.tistory.com/106</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;후..삽질하다가 너무 어이없게 해결해서 쓴다. 사실 내가 무지했던 거지만..&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;플러터에서 dio를 통해 스프링부트에서 간단하게 만든 api를 테스트하는 중이었다. 근데 어머나?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; SocketException:&amp;nbsp;Connection&amp;nbsp;refused&amp;nbsp;(OS&amp;nbsp;Error:&amp;nbsp;Connection&amp;nbsp;refused,&amp;nbsp;errno&amp;nbsp;=&amp;nbsp;111)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라는 오류가 떴다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;포스트맨으로 할 때도 됐고,,웹에서 할 때도 됐는데,,왜 안 될까.. CORS문제인가하면서 이상한 뻘짓들을 했는데&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;답은 너무나도 간단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;&lt;b&gt;안드로이드 에뮬레이터는 localhost를 사용할 수 없는 거&lt;/b&gt;&lt;/span&gt;였다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하..너무 당연하게 웹에서 구동하는 줄 알았따 ㅜㅠ 이거 땜에 뭔 고생을 한건지..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;암튼, localhost대신 10.0.2.2를 쓰면 된다고 한다&lt;/b&gt;&lt;/span&gt;. 물론 프로덕션으로 가게 되면 다른 걸 설정해야됨!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://developer.android.com/studio/run/emulator-networking?hl=ko&quot;&gt;https://developer.android.com/studio/run/emulator-networking?hl=ko&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689008133124&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;Android Emulator 네트워킹 설정 &amp;nbsp;|&amp;nbsp; Android 스튜디오 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;에뮬레이터는 앱에 복잡한 모델링 및 테스트 환경을 설정하는 데 사용할 수 있는 다목적 네트워킹 기능을 제공합니다.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/studio/run/emulator-networking?hl=ko&quot; data-og-url=&quot;https://developer.android.com/studio/run/emulator-networking?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/byHymp/hyTgXa1tUA/sfscDUZnY3KwlKkBpU5Gu0/img.png?width=1640&amp;amp;height=898&amp;amp;face=0_0_1640_898&quot;&gt;&lt;a href=&quot;https://developer.android.com/studio/run/emulator-networking?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/studio/run/emulator-networking?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/byHymp/hyTgXa1tUA/sfscDUZnY3KwlKkBpU5Gu0/img.png?width=1640&amp;amp;height=898&amp;amp;face=0_0_1640_898');&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;Android Emulator 네트워킹 설정 &amp;nbsp;|&amp;nbsp; Android 스튜디오 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;에뮬레이터는 앱에 복잡한 모델링 및 테스트 환경을 설정하는 데 사용할 수 있는 다목적 네트워킹 기능을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.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>App/Flutter</category>
      <category>안드로이드 에뮬레이터</category>
      <category>플러터</category>
      <author>Ray123</author>
      <guid isPermaLink="true">https://jofestudio.tistory.com/106</guid>
      <comments>https://jofestudio.tistory.com/106#entry106comment</comments>
      <pubDate>Tue, 11 Jul 2023 01:59:43 +0900</pubDate>
    </item>
  </channel>
</rss>