<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Backtony Dev</title>
    <link>https://backtony.tistory.com/</link>
    <description>Backend / 데이터베이스 / 클라우드 등의 주제로 지식을 공유합니다.</description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 23:13:14 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>backtony</managingEditor>
    <image>
      <title>Backtony Dev</title>
      <url>https://tistory1.daumcdn.net/tistory/6763067/attach/01c75511d4cf4b7a9dccdb14b7852e23</url>
      <link>https://backtony.tistory.com</link>
    </image>
    <item>
      <title>Github Action 알아보기</title>
      <link>https://backtony.tistory.com/94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 flow&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작동 단계&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XQg7Y/btsJDYKMOja/nlLSBKkClqkx7mPulofZCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XQg7Y/btsJDYKMOja/nlLSBKkClqkx7mPulofZCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XQg7Y/btsJDYKMOja/nlLSBKkClqkx7mPulofZCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXQg7Y%2FbtsJDYKMOja%2FnlLSBKkClqkx7mPulofZCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1780&quot; height=&quot;636&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;636&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;github push&lt;/li&gt;
&lt;li&gt;event trigger&lt;/li&gt;
&lt;li&gt;job 순서대로 작업 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 트리거&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;repository에서 일어나는 push, fort 등의 작업&lt;/li&gt;
&lt;li&gt;github project, issue에서 일어나는 작업&lt;/li&gt;
&lt;li&gt;사용자가 직접 트리거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 작성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br1gix/btsJENWijnC/h0kF3FxHp5hyiavvYdkAtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br1gix/btsJENWijnC/h0kF3FxHp5hyiavvYdkAtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br1gix/btsJENWijnC/h0kF3FxHp5hyiavvYdkAtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr1gix%2FbtsJENWijnC%2Fh0kF3FxHp5hyiavvYdkAtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1780&quot; height=&quot;636&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;636&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;b&gt;.github.workflows&lt;/b&gt; 폴더 하위에 github action으로 실행하고자 하는 일들을 파일별로 정의하면 별도의 설정 과정 없이 project의 actions 탭들을 통해 실행할 수 있습니다. actions 탭에서는 워크플로우 실행 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;workflow&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;workflow란 이벤트 발생 시, 어떠한 행위를 할 것인가에 대한 작업 정의합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmCIvq/btsJDtSkTWc/k288VDmI2BZNFZdh1LYGn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmCIvq/btsJDtSkTWc/k288VDmI2BZNFZdh1LYGn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmCIvq/btsJDtSkTWc/k288VDmI2BZNFZdh1LYGn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmCIvq%2FbtsJDtSkTWc%2Fk288VDmI2BZNFZdh1LYGn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1334&quot; height=&quot;722&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;722&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;runner : job별 별도의 공간에서 실행하는 작업 공간에 대한 정의로 작업 간 내용 공유는 기본적으로 제공되지 않습니다.&lt;/li&gt;
&lt;li&gt;job : 워크플로우 내 작동하는 작업 단위&lt;/li&gt;
&lt;li&gt;step : job 내 개별 수행되는 액션에 대한 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: Sample # workflow의 이름 =&amp;gt; github action 버튼에서 노출
on: workflow_dispatch # 이벤트 트리거 정의 =&amp;gt; 여기서는 수동 트리거
jobs:
  build: # job 1
    runs-on: ubuntu-latest # 해당 작업을 어떤 환경에서 수행할 것인지
    steps: # 순차적 step 정의
    - name: Checkout 
      uses: actions/checkout@v2
    - name: Build project
      run: |
        echo &quot;Build Project&quot;
    - name: Run tests
      run: |
        echo &quot;Run Test&quot;
  deploy: # job 2
    needs: build 
    runs-on: ubuntu-latest
    steps:
    - name: Deploy to production
      run: |
        echo &quot;Deploying to production server&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변수 사용법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;workflow 파일에서 사용되는 동적인 값으로 주로 환경 변수(environment variables), 시크릿(secret)값을 저장하고 사용합니다.&lt;/li&gt;
&lt;li&gt;workflow 파일에 env 키로 정의하여 단일 workflow에서 정의할 수도 있고 github organization이나 repository에서 정의하여 전체 workflow 내에서 global 하게 사용할 수도 있습니다.&lt;/li&gt;
&lt;li&gt;이외에도 Default로 제공되는 변수도 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;prefix로 GITHUB_X 또는 RUNNER_X로 네이밍 되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;workflow level에서 정의&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: variable-1
on: workflow_dispatch

env:
  fruit: Apple

jobs:
  build_1:
    runs-on: ubuntu-latest
    env:
      fruit: Orange
    steps:
    - name: Step 1 
      run: |
        echo &quot;Run Step 1, Make $fruit Juice!&quot; 
  build_2:
    runs-on: ubuntu-latest
    steps:
    - name: Step 1
      run: |
        echo &quot;Run Step 1, Make $fruit Juice!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build_1에서는 fruit이 재정의 되어있으므로 Orange가 출력되고 build_2에서는 바깥에 정의된 Apple을 사용하게 됩니다.&lt;/p&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;repo, organization level&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4026&quot; data-origin-height=&quot;2288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIayWC/btsJDPgjGJI/zSnQWMjbFJEaFOOlGg4K8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIayWC/btsJDPgjGJI/zSnQWMjbFJEaFOOlGg4K8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIayWC/btsJDPgjGJI/zSnQWMjbFJEaFOOlGg4K8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIayWC%2FbtsJDPgjGJI%2FzSnQWMjbFJEaFOOlGg4K8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4026&quot; height=&quot;2288&quot; data-origin-width=&quot;4026&quot; data-origin-height=&quot;2288&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;위 탭에서 전역으로 사용할 변수를 지정할 수 있으나 다음과 같은 제약 조건이 있습니다. secret 탭에서는 보안에 관련된 변수들을 지정해 두고 사용할 수 있습니다.&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;이름에는 영문 숫자 또는 언더바만 사용 가능&lt;/li&gt;
&lt;li&gt;공백 허용 X&lt;/li&gt;
&lt;li&gt;Default variable과 겹치지 않도록 권장&lt;/li&gt;
&lt;li&gt;숫자로 시작 불가&lt;/li&gt;
&lt;li&gt;대소문자를 구분 X&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: global variable
on: workflow_dispatch
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout 
      uses: actions/checkout@v2
    - name: Build project
      run: |
        echo &quot;Build Project ${{ vars.PROJECT_NAME }}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 사용하는 시점에는 &lt;b&gt;vars.&lt;/b&gt;를 붙여서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 간 데이터 전달&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$GITHUB_OUTPUT 을 활용해 전달합니다.&lt;/li&gt;
&lt;li&gt;Key=value 형태로 $GITHUB_OUTPUT 에 기록합니다.&lt;/li&gt;
&lt;li&gt;이후 &lt;code&gt;steps.&amp;lt;step_id&amp;gt;.outputs.&amp;lt;key&amp;gt;&lt;/code&gt; 로 데이터 접근합니다.&lt;/li&gt;
&lt;li&gt;기본적으로 job간의 공유는 불가능하고 외부 스토리지나 Artifact를 활용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: Step Output
on: workflow_dispatch
jobs:
  test-job:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout 
      uses: actions/checkout@v2
    - id: generate-random-id
      name: Generate random string
      run: echo &quot;random_id=$RANDOM&quot; &amp;gt;&amp;gt; &quot;$GITHUB_OUTPUT&quot;
    - id: build-project
      name: Build project
      run: |
        echo &quot;Random ID: ${{ steps.generate-random-id.outputs.random_id }}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건과 연산자&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;startsWith : 특정 문자열로 시작 시, true로 반환&lt;/li&gt;
&lt;li&gt;endsWith : 특정 문자열로 끝날 시, true 반환&lt;/li&gt;
&lt;li&gt;format : 특정 포맷의 string 값을 변수로 변경&lt;/li&gt;
&lt;li&gt;join : array 배열의 문자열 값을 연결&lt;/li&gt;
&lt;li&gt;if: 조건문&lt;/li&gt;
&lt;li&gt;success, failure, canceled, always : 전 step의 상태에 따른 step 수행 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if문을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;name: if
on: workflow_dispatch
jobs:
  build:
    runs-on: ubuntu-latest
    if: ${{ github.event_name == 'workflow_dispatch' &amp;amp;&amp;amp; github.ref == 'refs/heads/master' }}
    steps:
      - name: Say Hello
        run: |
          echo &quot;Say Hello to $GITHUB_ACTOR&quot;    &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;issue를 활용할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: issue
on: 
  issues:
    types: [opened, edited, labeled, unlabeled]
jobs:
  auto-assignee:
    runs-on: ubuntu-latest
    permissions:
      issues: write
    if: ${{ contains(github.event.issue.labels.*.name, 'bug') }}
    steps:
    - name: Auto assign issue
      uses: pozil/auto-assign-issue@v1
      with:
        assignees: ${{ vars.BUG_HUNTERS }}&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;bug라는 라벨이 포함되어 있으면 assignees를 bug_hunters로 지정하는 job입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;workflow&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;trigger&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brrQOn/btsJDWfa46j/d7zupkq2xdnpKcEmtKvow0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brrQOn/btsJDWfa46j/d7zupkq2xdnpKcEmtKvow0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brrQOn/btsJDWfa46j/d7zupkq2xdnpKcEmtKvow0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrrQOn%2FbtsJDWfa46j%2Fd7zupkq2xdnpKcEmtKvow0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1084&quot; height=&quot;740&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;740&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;workflow가 위치한 repository에서 발행한 이벤트&lt;/li&gt;
&lt;li&gt;repository 외 관련 서비스에서 발생항 이벤트&lt;/li&gt;
&lt;li&gt;예약된 시간에 트리거&lt;/li&gt;
&lt;li&gt;수동으로 직접 이벤트 트리거(workflow_dispatch)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;repository에서 발생한 이벤트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;push, pull_request, fork, release, tag, workflow 등의 이벤트를 트리거로 사용할 수 있습니다. on절에 명시합니다. paths-ignore을 통해 무시할 수 있는 경우도 추가할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: branch push
on: 
  push:
    branches:
      - '!test-**'
      - '!main'
      - '*'
    paths-ignore:
      - 'README.md'
      - '.github/workflows/**'
jobs:
  build:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;workflow 연동&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: branch push
on: 
  push:
    tags:
      - 'v1.**'
jobs:
  build:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;name: workflow next
on: 
  workflow_run:
    workflows: [&quot;branch push&quot;]
    types:
      - completed
jobs:
  build:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;schedule&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: cron
on: 
  schedule:
  - cron: '*/5 * * * *'
jobs:
  build:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수동 트리거 input&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: PART3 - CH2 - Workflow Dispatch
on: 
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to deploy'
        required: true
        default: '1.0.0'

jobs:
  test-job:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - id: input
        name: input test
        run: echo &quot;input=${{ github.event.inputs.tag }}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github repo에서 수동으로 트리거하는 경우 input 값을 받아서 사용할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;artifact&lt;/h3&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;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7VlZm/btsJEqtwKGA/fkpswHgqn4INiMy9KYAh81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7VlZm/btsJEqtwKGA/fkpswHgqn4INiMy9KYAh81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7VlZm/btsJEqtwKGA/fkpswHgqn4INiMy9KYAh81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7VlZm%2FbtsJEqtwKGA%2FfkpswHgqn4INiMy9KYAh81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;484&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;GitHub Action에서는 아티팩트(artifacts)라는 기능을 통해 워크플로우(Workflow)가 실행하는 동안 이전에 동일 업로드된 아티팩트(artifacts)를 다운로드할 수 있게 지원합니다. Job 간의 특정 공유가 필요한 경우 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: artifact
on: workflow_dispatch
jobs:
  job-1:
    runs-on: ubuntu-latest
    steps:
    - name: Get Random UUID
      id: uuid
      run: echo &quot;::set-output name=uuid::$(uuidgen)&quot;
    - name: Print UUID
      run: |
        echo &quot;UUID: ${{ steps.uuid.outputs.uuid }}&quot;
        echo &quot;${{ steps.uuid.outputs.uuid }}&quot; &amp;gt; uuid.txt
    - name: Upload UUID
      uses: actions/upload-artifact@v3
      with:
        name: uuid
        path: uuid.txt
  job-2:
    runs-on: ubuntu-latest
    needs: job-1
    steps:
    - name: Download UUID
      uses: actions/download-artifact@v3
      with:
        name: uuid
    - name: Print UUID
      run: |
        echo &quot;UUID: $(cat uuid.txt)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;job&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: job sample
on: workflow_dispatch
jobs:
  build: # id : job에 대한 고유 식별자
    name: Build and Test # job의 이름으로 github ui 상으로 표기
    runs-on: ubuntu-latest # runs-on : runner에 대한 정의, 작업을 수행할 머신의 형식 설정
    defaults: # run단계에서 적용될 shell과 working directory 설정
      run:
        shell: bash
        working-directory: ./build_sample/python    
    steps: # job 내의 실행 step 설정
      - id: checkout # step 고유 식별자
        name: Checkout # step 이름으로 ui 상에 표기
        uses: actions/checkout@v2
      - id: build-test
        name: Run npm build
        run: | # 쉘을 사용하여 명령어 실행. 단일/다중 명령 실행 가능
          npm ci
          npm run build
      - name: Login to Docker Hub
        if: always() # if문 
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USER }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;runner&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 실행기(Runner)에서 코드를 다운로드하고, 빌드를 위한 소프트웨어 설치 및 다양한 사전 실행 작업 및 빌드 행위를 수행합니다. 그렇기 때문에 다양한 OS 환경을 지원합니다. runner는 크게 두 가지 방식으로 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Hosting Runners
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;github에서 무료로 제공하는 공식 실행기(runner)&lt;/li&gt;
&lt;li&gt;github에서 자체 관리하므로 최신 os와 보안 업데이트&lt;/li&gt;
&lt;li&gt;다양한 운영체제 환경과 버전에 맞춘 실행기&lt;/li&gt;
&lt;li&gt;동시에 실행할 수 있는 job에 제한이 있습니다.(월간 사용량, 계정 유형에 따라 상이)&lt;/li&gt;
&lt;li&gt;제공되는 환경
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ubuntu, windows, macOS&lt;/li&gt;
&lt;li&gt;3가지 모두 같은 방식으로 지원
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;latest 지정 : ex) ubuntu-latest&lt;/li&gt;
&lt;li&gt;버전 지정 : ex) ubuntu-22.04&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Self-hosted runners
&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;해당 인스턴스에 github runner를 설치&lt;/li&gt;
&lt;li&gt;등록하여 사용 가능한 OS가 제한이 있지만 대부분을 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;self-host 지정 &amp;amp; 사용&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4092&quot; data-origin-height=&quot;1376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbevLP/btsJDn5EQzz/JxfHR5ewiuqJ0zlXNeypUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbevLP/btsJDn5EQzz/JxfHR5ewiuqJ0zlXNeypUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbevLP/btsJDn5EQzz/JxfHR5ewiuqJ0zlXNeypUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbevLP%2FbtsJDn5EQzz%2FJxfHR5ewiuqJ0zlXNeypUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4092&quot; height=&quot;1376&quot; data-origin-width=&quot;4092&quot; data-origin-height=&quot;1376&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;settings 탭에서 self-host runner를 생성하면 가이드가 나와있으니 참고하여 github-runner로 사용할 인스턴스 내부에 github runner를 설치해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;name: Self-Hosted Runner
on: 
  workflow_dispatch:
jobs:
  build:
    name: Build on self-hosted
    runs-on: self-hosted
    steps:
      - name: Say Hello
        run: |
          echo &quot;Say Hello to $GITHUB_ACTOR&quot;    &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;matrix 사용법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: Multi Matrix Runner 
on: workflow_dispatch
jobs:
  build:
  name: Multi OS Build 
  strategy:
    matrix:
      os: [ubuntu-20.04, ubuntu-22.04]
      version: [10, 12, 14] 
  runs-on: ${{ matrix.os }} 
  steps:
    - id: checkout
      name: Checkout
      uses: actions/checkout@v2 
    - id: setup-node
      name: Setup Node.js
      uses: actions/setup-node@v2 
      with:
        node-version: ${{ matrix.version }}&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;matrix를 사용하면 여러 환경에 대해 여러 번 수행이 가능합니다. 위 케이스에서는 ubuntu-20.04 환경에서 10, 12, 14 변수를 차례로 사용하여 수행하고 ubuntu-22.04에서도 마찬가지입니다. 즉, 총 6번을 수행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job은 별도의 설정이 없다면 정의된 job들이 동시에 병렬 수행됩니다. 이를 제어하려면 &lt;b&gt;needs&lt;/b&gt; 키워드를 사용합니다. 만약 grouping 하여 group 내 포함된 job들끼리 동시성을 제어하고자 한다면 &lt;b&gt;group&lt;/b&gt; 키워드를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;needs를 사용한 의존성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;name: needs
on: workflow_dispatch
jobs:
  job-1:
    name: Concurrency Job Test 1
    runs-on: ubuntu-latest
    steps:
    - id: checkout
      name: Checkout 
      uses: actions/checkout@v3
    - id: test-1
      name: Test 1
      run: echo &quot;Test 1&quot;
  job-2:
    name: Concurrency Job Test 2
    runs-on: ubuntu-latest
    needs: job-1
    steps:
    - id: checkout
      name: Checkout 
      uses: actions/checkout@v3
    - id: test-2
      name: Test 2
      run: echo &quot;Test 2&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Group&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: Groups
on: workflow_dispatch
jobs:
  job-1:
    name: Concurrency Job Test 1
    runs-on: ubuntu-latest
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}-1
    steps:
    - id: checkout
      name: Checkout 
      uses: actions/checkout@v3
    - id: test-1
      name: Test 1
      run: echo &quot;Test 1&quot;
  job-2:
    name: Concurrency Job Test 2
    runs-on: ubuntu-latest
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}-1
    steps:
    - id: checkout
      name: Checkout 
      uses: actions/checkout@v3
    - id: test-2
      name: Test 2
      run: echo &quot;Test 2&quot;
  job-3:
    name: Concurrency Job Test 3
    runs-on: ubuntu-latest
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}-2
    steps:
    - id: checkout
      name: Checkout 
      uses: actions/checkout@v3
    - id: test-3
      name: Test 3
      run: echo &quot;Test 3&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job-1, job-2는 같은 그룹으로 매핑되어 있고 job-3는 별도의 group으로 매핑되어 있습니다. 따라서 job-3는 바로 수행되지만 job-1, 2는 동시 실행을 시도하게 되고, 그중 하나가 먼저 실행을 시작하고 나머지 하나는 앞선 job이 완료된 이후 수행됩니다. 의존성을 추가하고 싶다면 needs를 마찬가지로 추가해 줘서 순서를 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하고자 하는 행위가 Github이나 issue, discussion 등등이라면 해당 서비스에 접근하기 위해 권한이 필요로 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;permission
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;action : github action 작업 수행 권한 설정&lt;/li&gt;
&lt;li&gt;checks : 검사 실행&lt;/li&gt;
&lt;li&gt;contents : repository 콘텐츠 접근 작업&lt;/li&gt;
&lt;li&gt;discussions : discussion 접근&lt;/li&gt;
&lt;li&gt;id-token : OIDC 토큰을 가져오는 작업&lt;/li&gt;
&lt;li&gt;issues : issue 접근 작업&lt;/li&gt;
&lt;li&gt;pages : pages 접근 작업&lt;/li&gt;
&lt;li&gt;pull-requests : pr 관련 작업&lt;/li&gt;
&lt;li&gt;security-events : github 코드 검사, dependabot 경고 관련 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;권한 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;write : 쓰기&lt;/li&gt;
&lt;li&gt;read : 읽기&lt;/li&gt;
&lt;li&gt;none: 지정하지 않음. 기본적으로 permissions에 키를 지정하지 않으면 모두 none&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;permissions: 
  contents: read 
  pull-requests: write 
  issues: none&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;secret&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4270&quot; data-origin-height=&quot;2272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFClej/btsJCV9FkbY/9VWMUIkJXzS35BSHmKg4Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFClej/btsJCV9FkbY/9VWMUIkJXzS35BSHmKg4Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFClej/btsJCV9FkbY/9VWMUIkJXzS35BSHmKg4Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFClej%2FbtsJCV9FkbY%2F9VWMUIkJXzS35BSHmKg4Z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4270&quot; height=&quot;2272&quot; data-origin-width=&quot;4270&quot; data-origin-height=&quot;2272&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;secret 탭에서는 보안에 관련된 변수들을 지정해 두고 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: Github Secrets
on: workflow_dispatch
jobs:
  build:
    name: Build and Test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      actions: read
    steps:
    - name: Checkout 
      uses: actions/checkout@v2
    - name: Build project
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      run: |
        echo &quot;$SLACK_WEBHOOK_URL&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Github Action 토큰 인증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우(Workflow) 실행 시, GitHub 관련 서비스에 접근하여 작업을 처리해야 하는 경우 해당 서비스에 접근하기 위한 API인증절차가 필요합니다. GITHUB_TOKEN 변수를 사용하면 github action에서 토큰을 자동으로 생성해 주고 Job이 완료되거나 최대 24시간 내에 만료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github_token은 다음과 같은 권한 설정이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SCOPE&lt;/th&gt;
&lt;th&gt;허용&lt;/th&gt;
&lt;th&gt;제한&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;actions&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;checks&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contents&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deployments&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id-token&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;issues&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;packages&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pages&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pull-requests&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;repository-projects&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;security-events&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;statuses&lt;/td&gt;
&lt;td&gt;read/write&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: Github Token
on:
  - pull_request_target

jobs:
  triage:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/labeler@v4
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;workflow 캐시&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRbnCJ/btsJEoh6FRb/5QtVOKkfp6uWy5vkwD2mKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRbnCJ/btsJEoh6FRb/5QtVOKkfp6uWy5vkwD2mKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRbnCJ/btsJEoh6FRb/5QtVOKkfp6uWy5vkwD2mKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRbnCJ%2FbtsJEoh6FRb%2F5QtVOKkfp6uWy5vkwD2mKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1938&quot; height=&quot;716&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;716&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;반복적으로 다운로드하게 되는 종속성을 가진 패키지를 매번 다운로드하는 대신, 캐시 영역에 저장 후 캐시 히트 시 저장된 파일을 바로 사용하게 하여 패키지 설치 과정을 최소화할 수 있습니다. 캐싱을 사용하려면 github action에서 제공해 주는 cache action을 사용해야 합니다. cache action은 unique 키를 통해 캐시 공간 생성 및 히트 여부를 확인합니다. 패키지 매니저 캐싱을 위해서는 setup-XX action을 설정해야 사용 가능합니다. 캐시 설정에는 다음과 같은 설정이 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key(필수) : 캐시 저장 시 캐싱 여부를 결정하는 키값으로 최대 512자&lt;/li&gt;
&lt;li&gt;path(필수) : 캐시 히트 되었다고 판단 시, runner에 캐싱된 파일들을 복원하는 경로로 절대/상대 경로 모두 지정 가능&lt;/li&gt;
&lt;li&gt;restore-keys(옵션) : 캐싱 키를 찾지 못할 경우, 차선으로 restore-keys 값에 선언된 순서대로 키를 추가 확인(키가 완전히 매치되지 않아도 prefix를 확인하며 추가로 히트 여부를 확인)&lt;/li&gt;
&lt;li&gt;enableCrossOsArchive(옵션) : 다른 OS의 Runner에서 생성된 캐시라도 캐싱되면 사용 가능하도록 설정&lt;/li&gt;
&lt;li&gt;cache-hit : cache에 설정된 키와 일치하여 히트 여부 확인하는 것으로 히트된 경우 true 값을 리턴합니다.&lt;/li&gt;
&lt;li&gt;cache version : 캐시 된 데이터를 저장하는 데 사용된 압축툴과 디렉터리 경로를 조합하여 생성된 hash값으로 runner가 다른 OS인 경우 캐시 key가 동일해도 복원되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;name: Caching Dependencies
on: workflow_dispatch
jobs:
  build-using-cache:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Move JS Sample code
        run: |
          mv cache_sample/* ./
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          registry-url: 'https://registry.npmjs.org/'
      - id: node-cache
        name: Cache node modules
        uses: actions/cache@v3
        env:
          cache-name: cache-node-modules
        # node_mudules 위치에 있는 것을 캐싱
        with:
          path: node_modules
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-
      - name: Install dependencies
        # 캐시가 히트되지 않은 경우 install
        if: steps.node-cache.outputs.cache-hit != 'true'
        run: npm install
        # 캐시가 히트되었을때 잘 가져왔는지 확인
      - name: List packages from node modules
        continue-on-error: true
        run: npm list&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;캐시 사용 시 유의 사항&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;7일 이상 액세스 되지 않은 캐시는 자동으로 삭제&lt;/li&gt;
&lt;li&gt;저장할 수 있는 캐시 수는 제한이 없으나 모든 캐시 총합은 최대 10GB&lt;/li&gt;
&lt;li&gt;저장소가 한계에 도달 시 가장 오래된 캐시부터 삭제 수행&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>ETC</category>
      <category>github</category>
      <category>GitHubAction</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/94</guid>
      <comments>https://backtony.tistory.com/94#entry94comment</comments>
      <pubDate>Mon, 16 Sep 2024 10:32:45 +0900</pubDate>
    </item>
    <item>
      <title>Coroutine 환경에서 Spring AOP 사용하기 (with. tx와 같이 사용시 버그)</title>
      <link>https://backtony.tistory.com/93</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는 &lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/spring/coroutine-aop/spring-reactive-aop-transaction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Coroutine 환경에서 Spring AOP 사용하기&lt;/h2&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;1594&quot; data-origin-height=&quot;1078&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cR2nBq/btsIx8BKOs6/BuBRxPBdSXvDe2tsxAyhm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cR2nBq/btsIx8BKOs6/BuBRxPBdSXvDe2tsxAyhm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cR2nBq/btsIx8BKOs6/BuBRxPBdSXvDe2tsxAyhm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcR2nBq%2FbtsIx8BKOs6%2FBuBRxPBdSXvDe2tsxAyhm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;1078&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;1078&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;MVC 환경에서 AOP를 적용할 때는 위와 같이 적용하게 됩니다.&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;2120&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxkmRJ/btsIx8BKOCl/okFIjMoSxe8lYP8ZHeZt7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxkmRJ/btsIx8BKOCl/okFIjMoSxe8lYP8ZHeZt7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxkmRJ/btsIx8BKOCl/okFIjMoSxe8lYP8ZHeZt7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxkmRJ%2FbtsIx8BKOCl%2FokFIjMoSxe8lYP8ZHeZt7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2120&quot; height=&quot;1166&quot; data-origin-width=&quot;2120&quot; data-origin-height=&quot;1166&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;일반적으로 AOP에서 advice를 정의할 때, 타겟 메서드 전후로 특정 서비스의 메서드를 호출하는 경우가 있을 수 있습니다. 여기서 Coroutine을 사용한다면 타겟 메서드 호출 전후로 suspend 함수를 호출해야 하는 경우가 발생합니다. advice에서 suspend 함수를 호출하게 되면 위와 같이 suspend 함수를 호출하기 위해서는 현재 함수가 suspend 함수이거나 coroutine 환경 이어야 된다는 메시지가 표기됩니다.&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;1540&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csoPX4/btsIyBwEHug/UDb0cxBeDAiIKTAhGrhIJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csoPX4/btsIyBwEHug/UDb0cxBeDAiIKTAhGrhIJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csoPX4/btsIyBwEHug/UDb0cxBeDAiIKTAhGrhIJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsoPX4%2FbtsIyBwEHug%2FUDb0cxBeDAiIKTAhGrhIJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1540&quot; height=&quot;1080&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 에러 메시지에서 제시한 해결책 중에 간단해보이는 해결책인 advice 함수에 suspend 키워드를 붙이고 실행시켜 보면 아래와 같은 메시지가 표기됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3332&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp14pS/btsIybL0Lj8/lilAnBwJ9bOQrOt5BobOyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp14pS/btsIybL0Lj8/lilAnBwJ9bOQrOt5BobOyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp14pS/btsIybL0Lj8/lilAnBwJ9bOQrOt5BobOyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp14pS%2FbtsIybL0Lj8%2FlilAnBwJ9bOQrOt5BobOyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3332&quot; height=&quot;292&quot; data-origin-width=&quot;3332&quot; data-origin-height=&quot;292&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;2개의 인자를 예상했는데 1개의 인자만 들어왔다는 에러 메시지가 표기됩니다. coroutine의 경우 CPS 패턴을 사용하기 때문에 suspend 키워드가 붙은 함수는 컴파일되면 함수의 마지막 인자로 continuation이 추가되게 되는데 아직까지 advice 함수에서는 이를 지원하지 않기 때문에 해당 메시지가 표기된 것으로 보입니다. CPS 패턴은 포스팅 가장 마지막에 따로 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 에러 메시지에서 보여준 다른 해결책인 coroutine 환경에서 suspend 함수를 호출해야할 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2820&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3m3rA/btsIzbKYr5O/Wh2nE6s1i6hzVuBjLP6lZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3m3rA/btsIzbKYr5O/Wh2nE6s1i6hzVuBjLP6lZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3m3rA/btsIzbKYr5O/Wh2nE6s1i6hzVuBjLP6lZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3m3rA%2FbtsIzbKYr5O%2FWh2nE6s1i6hzVuBjLP6lZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2820&quot; height=&quot;958&quot; data-origin-width=&quot;2820&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/22462&quot;&gt;https://github.com/spring-projects/spring-framework/issues/22462&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Spring AOP에서 coroutine 지원에 대한 논의는 오래전부터 있었습니다. 해당 이슈의 코멘트를 보면 아래와 같이 advice에서 coroutine을 사용할 수 있는 샘플도 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// https://gist.github.com/pjanczyk/5d958821bafd911a5996bc0b66788ea3#file-aopwithcoroutines-kt-L92

// continuation을 사용하여 suspend 함수를 호출하는 기능을 제공하는 확장 함수
fun ProceedingJoinPoint.runCoroutine(
    block: suspend () -&amp;gt; Any?,
): Any? = block.startCoroutineUninterceptedOrReturn(this.coroutineContinuation())

// 마지막 인자인 continuation 을 가져오는 확장 함수
@Suppress(&quot;UNCHECKED_CAST&quot;)
fun ProceedingJoinPoint.coroutineContinuation(): Continuation&amp;lt;Any?&amp;gt; {
    return this.args.last() as Continuation&amp;lt;Any?&amp;gt;
}

// continuation을 제외한 인자를 가져오는 확장 함수
fun ProceedingJoinPoint.coroutineArgs(): Array&amp;lt;Any?&amp;gt; {
    return this.args.sliceArray(0 until this.args.size - 1)
}

// 실질적인 target 함수 호출에 호출 continuation을 전달하는 확장 함수
suspend fun ProceedingJoinPoint.proceedCoroutine(
    args: Array&amp;lt;Any?&amp;gt; = this.coroutineArgs(),
): Any? = suspendCoroutineUninterceptedOrReturn { continuation -&amp;gt;
    this.proceed(args + continuation)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 간단하게 설명해보자면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;startCoroutineUninterceptedOrReturn&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ngIRz/btsIyzlkcIX/Trw1Fpy9I9YIwtHUGgFIC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ngIRz/btsIyzlkcIX/Trw1Fpy9I9YIwtHUGgFIC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ngIRz/btsIyzlkcIX/Trw1Fpy9I9YIwtHUGgFIC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FngIRz%2FbtsIyzlkcIX%2FTrw1Fpy9I9YIwtHUGgFIC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2170&quot; height=&quot;552&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starts an unintercepted coroutine without a receiver and with result type T and executes it until its first suspension. Returns the result of the coroutine or throws its exception if it does not suspend or COROUTINE_SUSPENDED if it suspends. In the latter case, the completion continuation is invoked when the coroutine completes with a result or an exception.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달받은 Continuation을 가지고 suspend 함수 block을 실행할 수 있도록 합니다.&lt;/p&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;suspendCoroutineUninterceptedOrReturn&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8yliT/btsIx1QaB7l/zBuC6sLBO74qLkpmN1MG71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8yliT/btsIx1QaB7l/zBuC6sLBO74qLkpmN1MG71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8yliT/btsIx1QaB7l/zBuC6sLBO74qLkpmN1MG71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8yliT%2FbtsIx1QaB7l%2FzBuC6sLBO74qLkpmN1MG71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2596&quot; height=&quot;454&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://myungpyo.medium.com/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B3%B5%EC%8B%9D-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%9D%BD%EA%B8%B0-part-2-dive-1-4c468828319&quot;&gt;코루틴 공식 가이드 자세히 읽기&lt;/a&gt;&lt;br /&gt;전달 된 코드 블록에서 호출 코루틴(Continuation) 정보에 접근할 수 있도록 해줍니다. 또한, 전달된 코드 블록에서 COROUTINE_SUSPENDED 라는 미리 정의된 값을 반환할 경우에는 코루틴이 처리를 위해 시간이 필요하여 값을 바로 반환하지 않고 처리가 완료되면 continuation 파라미터를 통해 결과를 전달할 것임을 나타내고, 그 이외의 값을 반환할 경우에는 중단 없이 바로 결과 값을 반환한 것을 나타냅니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jointPoint.proceed()의 타겟 메서드가 suspend 함수이기 때문에 마지막 인자로 continuation을 넘겨주기 위해 사용한다고 보시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장함수를 사용해서 아래와 같이 처리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Aspect
@Component
class TestAspect(
    private val suspendService: SuspendService
) {

    @Around(&quot;@annotation(com.example.aopwithtransaction.aop.Logging)&quot;)
    fun logging(joinPoint: ProceedingJoinPoint): Any? {

        return joinPoint.runCoroutine {
            suspendService.intercept(&quot;전처리&quot;)

            val result = joinPoint.proceedCoroutine()

            suspendService.intercept(&quot;후처리&quot;)

            result
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 처리하면 Coroutine 환경에서도 AOP를 사용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;b&gt;굳이 이렇게 까지 해야할까. 너무 복잡한 것 같은데&lt;/b&gt; 라는 생각이 들었습니다. (suspendCoroutineUninterceptedOrReturn과 startCoroutineUninterceptedOrReturn 두 함수의 사용이 코드의 복잡성을 증가시킨다는 느낌이 들었습니다.)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6.1.0 버전 이상 부터는&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring에서 coroutine의 호환성을 대응하기 시작하면서 spring aop에 관련된 대응도 진행되기 시작했습니다.&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;2324&quot; data-origin-height=&quot;1778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kTode/btsIx2BwIXm/TsVnXKp2BIj69EZdZZHuO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kTode/btsIx2BwIXm/TsVnXKp2BIj69EZdZZHuO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kTode/btsIx2BwIXm/TsVnXKp2BIj69EZdZZHuO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkTode%2FbtsIx2BwIXm%2FTsVnXKp2BIj69EZdZZHuO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2324&quot; height=&quot;1778&quot; data-origin-width=&quot;2324&quot; data-origin-height=&quot;1778&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;코멘트를 확인해 보면 6.1.0-RC1 버전부터는 advice에서 타겟메서드가 suspend 함수인 경우 Mono를 리턴한다고 합니다.&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;2300&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIEdjF/btsIxZSmC7P/nWFRM6tgDkH8IxdMsYNAnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIEdjF/btsIxZSmC7P/nWFRM6tgDkH8IxdMsYNAnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIEdjF/btsIxZSmC7P/nWFRM6tgDkH8IxdMsYNAnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIEdjF%2FbtsIxZSmC7P%2FnWFRM6tgDkH8IxdMsYNAnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2300&quot; height=&quot;720&quot; data-origin-width=&quot;2300&quot; data-origin-height=&quot;720&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;실제로 advice 타겟 메서드가 suspend 함수인 경우 Mono를 리턴하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;advice 함수에서 mono를 반환해도 문제가 없다면 &lt;b&gt;advice 함수에서 suspend 함수를 실행하는 mono를 반환하면 앞서 구현한 방식보다 더 간단하게 처리할 수 있지 않을까?&lt;/b&gt;&amp;nbsp;라는 생각이 들었습니다. 이에 대한 힌트는 Webflux가 어떻게 coroutine을 처리하는지에서 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring webflux의 HTTP 요청은 DisptacherHandler -&amp;gt; HandlerAdaptor 순으로 받게 됩니다.&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;3754&quot; data-origin-height=&quot;2006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LOdfr/btsIx1Jpk4F/cCZLl8xFABQRPpG1SiyLA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LOdfr/btsIx1Jpk4F/cCZLl8xFABQRPpG1SiyLA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LOdfr/btsIx1Jpk4F/cCZLl8xFABQRPpG1SiyLA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLOdfr%2FbtsIx1Jpk4F%2FcCZLl8xFABQRPpG1SiyLA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3754&quot; height=&quot;2006&quot; data-origin-width=&quot;3754&quot; data-origin-height=&quot;2006&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;HandlerAdaptor의 구현체인 RequestMappingHandlerAdpater를 보면 요청에 따른 매핑 method를 찾아서 invoke하는 부분이 있습니다.&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;2794&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3jvWi/btsIzc4clEV/4p2npPbj9gKWb5D8WJEbv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3jvWi/btsIzc4clEV/4p2npPbj9gKWb5D8WJEbv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3jvWi/btsIzc4clEV/4p2npPbj9gKWb5D8WJEbv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3jvWi%2FbtsIzc4clEV%2F4p2npPbj9gKWb5D8WJEbv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2794&quot; height=&quot;870&quot; data-origin-width=&quot;2794&quot; data-origin-height=&quot;870&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;invoke를 더 타고 들어가다 보면 suspend 함수인 경우 CoroutinesUtils.invokeSuspendingFunction를 호출하게 됩니다. CoroutinesUtils.invokeSuspendingFunction를 따라가다 보면 아래와 같은 코드를 보게 됩니다.&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;2518&quot; data-origin-height=&quot;2450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3xJ45/btsIzWzP3Eo/f3i0kni9NWch6mdvS0aMRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3xJ45/btsIzWzP3Eo/f3i0kni9NWch6mdvS0aMRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3xJ45/btsIzWzP3Eo/f3i0kni9NWch6mdvS0aMRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3xJ45%2FbtsIzWzP3Eo%2Ff3i0kni9NWch6mdvS0aMRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2518&quot; height=&quot;2450&quot; data-origin-width=&quot;2518&quot; data-origin-height=&quot;2450&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;b&gt;MonoKt.mono&lt;/b&gt; 메서드는 코루틴 컨텍스트와 람다 함수를 받아 Mono를 생성합니다. 람다 내부에서 &lt;b&gt;KCallables.callSuspendBy(function, argMap, continuation)&lt;/b&gt;를 호출하여 실제 suspend 함수를 실행합니다. 이 과정을 통해 suspend 함수가 Project Reactor의 Mono로 변환됩니다. 즉, 코루틴 기반의 비동기 코드를 리액티브 스트림과 호환되는 형태로 변환할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 다시 돌아와서 Advice 함수를 다음과 같이 수정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byb8dh/btsIyCh0mTp/e796Dmdh8KKMCq5YFgYQRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byb8dh/btsIyCh0mTp/e796Dmdh8KKMCq5YFgYQRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byb8dh/btsIyCh0mTp/e796Dmdh8KKMCq5YFgYQRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyb8dh%2FbtsIyCh0mTp%2Fe796Dmdh8KKMCq5YFgYQRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1826&quot; height=&quot;1586&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1586&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;더 이상 이전처럼 확장함수를 사용할 필요 없이 mono를 한번 감싸주면 잘 동작하는 것을 확인할 수 있습니다 .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional과 AOP를 사용하는 경우 AOP가 무시되는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class TargetService() {
    private val log = KotlinLogging.logger { }

    @Logging
    @Transactional
    suspend fun aop(): String {
        delay(100)
        log.info { &quot;aop target method call&quot; }

        return &quot;ok&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Aspect
//@Order(1) 
@Component
class LoggingAspect {

    private val log = KotlinLogging.logger {}

    @Around(&quot;@annotation(com.example.aopwithtransaction.aop.Logging)&quot;)
    fun logging(joinPoint: ProceedingJoinPoint): Any? {
        return mono {
            log.info { &quot;Aop Logging started&quot; }

            val result = joinPoint.proceed().let { result -&amp;gt;
                if (result is Mono&amp;lt;*&amp;gt;) {
                    result.awaitSingleOrNull()
                } else {
                    result
                }
            }

            log.info { &quot;Aop Logging completed&quot; }

            result
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Logging 애노테이션과 @Transactional을 함께 붙여서 실행시켜보면 advice가 동작하지 않고 @Order 애노테이션을 명시해야만 advice가 동작하는 이슈가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/33095&quot;&gt;https://github.com/spring-projects/spring-framework/issues/33095&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인파악이 잘 안되서 spring에 이슈를 남겼고 버그라고 답변을 받았습니다. 6.1.11 버전에서 수정되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPS 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin 은 비동기 프로그래밍을 지원을 위해 CSP(Communicating Sequential Process) 기법을 사용합니다. Suspend 함수는 Suspension point(중단점)을 제공하여 함수가 중단될 수 있도록 하며, 이를 통하여 blocking 로직으로 부터 벗어나서 비동기로 동작할 수 있도록 합니다. Kotlin 은 이 중단점을 제공하기 위해 Suspend 함수를 컴파일 할 때, 함수 마지막 인자로 Continuation 이라는 객체를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// Kotlin 
suspend fun makeHelloWorld(): String

// 컴파일된 코드 
fun makeHelloWorld(@NotNull `$completion`: Continuation?): Any?&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;continuation은 다음과 같은 구조를 가지고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface Continuation&amp;lt;in T&amp;gt; {
    val context: CoroutineContext
    fun resumeWith(result: Result&amp;lt;T&amp;gt;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;continuation은 resumeWith 호출로 결과값을 전달해 원래 함수를 재개시키는 기능을 제공합니다. 함수를 일시중지 시키고 재개하려면 어디까지 진행했고 어디서부터 다시 재개해야하는지 알고있어야 합니다. 이는 코틀린 컴파일러가 코드를 변환할때 label을 두어 표시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun makeHelloWorld(): String {
    val msg = &quot;hello world&quot;
    delay(1000)
    return msg
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 디컴파일해보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceKSwC/btsIzopOry8/u4fhE0opEKxUTRfb2pc7N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceKSwC/btsIzopOry8/u4fhE0opEKxUTRfb2pc7N0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceKSwC/btsIzopOry8/u4fhE0opEKxUTRfb2pc7N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceKSwC%2FbtsIzopOry8%2Fu4fhE0opEKxUTRfb2pc7N0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2022&quot; height=&quot;1294&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;1294&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;위 그림을 보면 중단되는 포인트(suspension point)마다 label을 통해 case로 나누고 해당 케이스마다 결과값들을 continuation에 저장하여 상태관리하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이렇게 만들어진 함수는 continuation에서 어떻게 호출되는지를 Continuation 의 구현체인 BaseContinuationImpl의 resumeWith로 살펴보겠습니다.&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;2740&quot; data-origin-height=&quot;2320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crldCy/btsIAdahaEJ/ZHfUzyeocNPjG9Imq9XuA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crldCy/btsIAdahaEJ/ZHfUzyeocNPjG9Imq9XuA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crldCy/btsIAdahaEJ/ZHfUzyeocNPjG9Imq9XuA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrldCy%2FbtsIAdahaEJ%2FZHfUzyeocNPjG9Imq9XuA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2740&quot; height=&quot;2320&quot; data-origin-width=&quot;2740&quot; data-origin-height=&quot;2320&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;invokeSuspend 함수 호출부가 바로 앞서 만들었던 makeHelloWorld 함수를 호출하는 부분입니다. 해당 함수의 리턴 값이 COROUTINE_SUSPENDED라면 해당 함수를 return해버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 중단지점이 없는 경우라면 while문의 반복으로 앞선 switch case문들이 계속 돌아가면서 타겟함수의 수행이 완료되는 것이고, 중단지점이 존재한다면 COROUTINE_SUSPENDED를 리턴받으면서 특정 switch문까지만 진행되고 일시중지되었다가 해당 작업이 완료되면 코루틴 프레임워크에 의해 자동으로 resumeWith가 호출되면서 다시 작업이 이어지는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/22462#issuecomment-1683102470&quot;&gt;spring 이슈&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hikaMaeng/kotlinCoroutineKR#%EC%BD%94%EB%A3%A8%ED%8B%B4-%EB%82%B4%EC%9E%A5%ED%95%A8%EC%88%98&quot;&gt;kotlinCoroutineKR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myungpyo.medium.com/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B3%B5%EC%8B%9D-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%9D%BD%EA%B8%B0-part-2-dive-1-4c468828319&quot;&gt;코루틴 공식 가이드 자세히 읽기 &amp;mdash; Part 2&amp;mdash; Dive 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myungpyo.medium.com/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B3%B5%EC%8B%9D-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%9D%BD%EA%B8%B0-part-5-dive-2-b4e7a1626c59&quot;&gt;코루틴 공식 가이드 자세히 읽기 &amp;mdash; Part 5 &amp;mdash; Dive 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>AOP</category>
      <category>Coroutine</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/93</guid>
      <comments>https://backtony.tistory.com/93#entry93comment</comments>
      <pubDate>Sun, 14 Jul 2024 11:02:39 +0900</pubDate>
    </item>
    <item>
      <title>Oauth2 Authorization 서버 구축하기</title>
      <link>https://backtony.tistory.com/92</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;공부한 내용을 정리하는&lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/spring/oauth/spring-oauth2-authorization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;2810&quot; data-origin-height=&quot;1512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DMqIm/btsIqdbIU7u/BQE0x2u99knqws2pcccfy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DMqIm/btsIqdbIU7u/BQE0x2u99knqws2pcccfy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DMqIm/btsIqdbIU7u/BQE0x2u99knqws2pcccfy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDMqIm%2FbtsIqdbIU7u%2FBQE0x2u99knqws2pcccfy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2810&quot; height=&quot;1512&quot; data-origin-width=&quot;2810&quot; data-origin-height=&quot;1512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Open Authorization 2.0은 &lt;b&gt;인가를 위한 개방형 표준 프로토콜입니다.&lt;/b&gt; 이 프로토콜에서는 third-party 애플리케이션이 사용자의 리소스에 접근하기 위한 절차를 정의하고 서비스 제공자의 API를 사용할 수 있는 권한을 부여합니다. 대표적으로 네이버 로그인, 구글 로그인과 같은 소셜 미디어 간편 로그인이 있습니다. OAuth2.0을 사용해 third-party 애플리케이션이 사용자의 소셜미디어 프로필 정보에 접근할 수 있도록 합니다. 즉, &lt;b&gt;OAuth 2.0는 사용자가 애플리케이션(클라이언트)에게 자신을 대신하여 자원 서버(Resource Server)에서 특정 자원에 접근할 수 있는 권한을 부여하는 것을 목표로 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 인증(authentication) 프로토콜은 사용자의 신원을 확인하는 데 중점을 둡니다. OAuth 2.0 자체는 인증 프로토콜이 아니지만, OpenID Connect와 같은 프로토콜은 OAuth 2.0를 기반으로 하여 인증을 제공하는 기능을 추가합니다. 이 때문에 OAuth2.0을 인증 및 권한 부여를 위한 개방형 표준 프로토콜이라고 하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;2754&quot; data-origin-height=&quot;1218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S3nGv/btsIqD2jqn9/7kPbTiePoxxghzydl9Rahk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S3nGv/btsIqD2jqn9/7kPbTiePoxxghzydl9Rahk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S3nGv/btsIqD2jqn9/7kPbTiePoxxghzydl9Rahk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS3nGv%2FbtsIqD2jqn9%2F7kPbTiePoxxghzydl9Rahk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2754&quot; height=&quot;1218&quot; data-origin-width=&quot;2754&quot; data-origin-height=&quot;1218&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;OAuth 2.0을 구성하는 4가지 역할은 다음과 같습니다.&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;리소스 소유자(Resource Owner): OAuth 2.0 프로토콜을 사용하여 보호되는 리소스에 대한 액세스 권한을 부여하는 사용자입니다. 클라이언트를 인증(Authorize)하는 역할을 수행합니다. 예를 들어 네이버 로그인에서 네이버 아이디를 소유하고 third-party 애플리케이션(클라이언트)에 네이버 아이디로 소셜 로그인 인증을 하는 사용자를 의미합니다.&lt;/li&gt;
&lt;li&gt;클라이언트(Client): OAuth 2.0을 사용하여 리소스에 접근하려는 third-party 애플리케이션이나 서비스입니다.&lt;/li&gt;
&lt;li&gt;권한 서버(Authorization Server): 권한 서버는 클라이언트가 리소스 소유자의 권한을 얻을 수 있도록 도와주는 서버입니다. 권한 서버는 사용자 인증, 권한 부여 및 토큰 발급을 관리합니다.&lt;/li&gt;
&lt;li&gt;리소스 서버(Resource Server): 리소스 서버는 보호되는 리소스를 호스팅 하는 서버로, 액세스를 허용하거나 거부합니다. 이 서버는 OAuth 2.0 토큰을 사용하여 클라이언트에게 리소스에 액세스 할 권한을 부여하고 실제 데이터를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;용어&lt;/h2&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;액세스 토큰(Access Token): 클라이언트가 리소스 서버의 리소스에 접근하기 위한 권한을 부여받는 토큰입니다. 액세스 토큰은 권한 서버로부터 발급되며, 일반적으로 제한된 유효 기간을 가지고 있습니다.&lt;/li&gt;
&lt;li&gt;리프레시 토큰(Refresh Token): 리프레시 토큰은 액세스 토큰의 유효 기간이 만료된 후 새로운 액세스 토큰을 받기 위한 토큰입니다. 이를 통해 사용자는 다시 로그인할 필요 없이 토큰 유효 시간 갱신만으로 계속 애플리케이션을 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;범위(Scope): 범위는 클라이언트가 리소스에 대한 어떤 작업을 수행할 수 있는지를 정의하는 문자열입니다. 범위는 권한 서버에 의해 정의되며, 클라이언트는 특정 범위의 액세스 권한을 요청할 수 있습니다.&lt;/li&gt;
&lt;li&gt;인증 코드(Authorization Code): 인증 코드는 클라이언트가 액세스 토큰을 얻기 위한 중간 단계로 사용되는 코드입니다. 인증 코드 부여(Authorization Code Grant) 방식을 통해 권한 서버로부터 발급되며, 이를 사용하여 액세스 토큰과 리프레시 토큰을 얻을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;권한 부여 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0에서는 클라이언트 애플리케이션이 리소스 서버에 접근할 권한을 부여하고 관리하기 위해 다양한 권한 부여 방식이 제공됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization Code Grant Type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞서 용어에서 언급된 인증 코드(Authorization Code)를 사용하는 방식으로 가장 보안이 높은 방식입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Client Credentials Grant Type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;client Id와 client secret만 있다면 access token을 발급받는 방식으로 보통 server to server 관계에서 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PKCE-enhanced Authorization Code Grant Type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 교환을 위한 증명키로서 CSRF 및 권한부여 코드 삽입 공격을 방지하기 위한 Authorization Code Grant Type의 확장 버전입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Refresh Token Grant Type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;access token에는 만료기한이 있기 때문에 만료가 되었을 경우 다시 인증과정을 처음부터 거치지 않고 refresh을 사용하여 바로 access token을 다시 발급받을 수 있는 방식입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization Code, Resource Owner Password Type 에서 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 구조에서 Authorization Code Grant Type 방식을 채택하면 아래와 같은 흐름으로 이뤄집니다.&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;1047&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1uUom/btsIruKnOxa/ylLudHDUwY0hEV78FKofq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1uUom/btsIruKnOxa/ylLudHDUwY0hEV78FKofq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1uUom/btsIruKnOxa/ylLudHDUwY0hEV78FKofq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1uUom%2FbtsIruKnOxa%2FylLudHDUwY0hEV78FKofq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;655&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring OAuth2.0 구축하기&lt;/h2&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;1276&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FNE9n/btsIrcQKI8M/SoYmwuIJnykzUewPQoYbJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FNE9n/btsIrcQKI8M/SoYmwuIJnykzUewPQoYbJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FNE9n/btsIrcQKI8M/SoYmwuIJnykzUewPQoYbJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFNE9n%2FbtsIrcQKI8M%2FSoYmwuIJnykzUewPQoYbJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1276&quot; height=&quot;532&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;532&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;위 4개의 컴포넌트를 사용한 OAuth 2.0을 데모로 구축해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;├── client-login-server
├── hello-authorization-server
├── hello-resource-server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 모듈 구조는 위와 같습니다. Client A Frontend는 따로 구축하지 않고 chrome을 활용하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Authorization Server 구축하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle.kts&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    // authorization server
    implementation(&quot;org.springframework.boot:spring-boot-starter-oauth2-authorization-server&quot;)

    // for register user
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-validation&quot;)

    // redis
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis&quot;)
    implementation(&quot;org.springframework.session:spring-session-data-redis&quot;)
    implementation(&quot;org.apache.commons:commons-pool2:2.12.0&quot;)

    // db
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
    implementation(&quot;com.mysql:mysql-connector-j&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;authorization-server 의존성을 추가합니다. Authorization server에 사용자를 가입시키기 위한 web과 db 의존성도 추가합니다. redis 의존성을 추가하는 이유는 이후에 다시 설명하겠습니다.&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;schema.sql&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE if not exists oauth2_authorization_consent (
                                              registered_client_id varchar(100) NOT NULL,
                                              principal_name varchar(200) NOT NULL,
                                              authorities varchar(1000) NOT NULL,
                                              PRIMARY KEY (registered_client_id, principal_name)
);


CREATE TABLE if not exists oauth2_authorization (
                                      id varchar(100) NOT NULL,
                                      registered_client_id varchar(100) NOT NULL,
                                      principal_name varchar(200) NOT NULL,
                                      authorization_grant_type varchar(100) NOT NULL,
                                      authorized_scopes varchar(1000) DEFAULT NULL,
                                      attributes blob DEFAULT NULL,
                                      state varchar(500) DEFAULT NULL,
                                      authorization_code_value blob DEFAULT NULL,
                                      authorization_code_issued_at timestamp DEFAULT NULL,
                                      authorization_code_expires_at timestamp DEFAULT NULL,
                                      authorization_code_metadata blob DEFAULT NULL,
                                      access_token_value blob DEFAULT NULL,
                                      access_token_issued_at timestamp DEFAULT NULL,
                                      access_token_expires_at timestamp DEFAULT NULL,
                                      access_token_metadata blob DEFAULT NULL,
                                      access_token_type varchar(100) DEFAULT NULL,
                                      access_token_scopes varchar(1000) DEFAULT NULL,
                                      oidc_id_token_value blob DEFAULT NULL,
                                      oidc_id_token_issued_at timestamp DEFAULT NULL,
                                      oidc_id_token_expires_at timestamp DEFAULT NULL,
                                      oidc_id_token_metadata blob DEFAULT NULL,
                                      refresh_token_value blob DEFAULT NULL,
                                      refresh_token_issued_at timestamp DEFAULT NULL,
                                      refresh_token_expires_at timestamp DEFAULT NULL,
                                      refresh_token_metadata blob DEFAULT NULL,
                                      user_code_value blob DEFAULT NULL,
                                      user_code_issued_at timestamp DEFAULT NULL,
                                      user_code_expires_at timestamp DEFAULT NULL,
                                      user_code_metadata blob DEFAULT NULL,
                                      device_code_value blob DEFAULT NULL,
                                      device_code_issued_at timestamp DEFAULT NULL,
                                      device_code_expires_at timestamp DEFAULT NULL,
                                      device_code_metadata blob DEFAULT NULL,
                                      PRIMARY KEY (id)
);

CREATE TABLE if not exists oauth2_registered_client (
                                          id varchar(100) NOT NULL,
                                          client_id varchar(100) NOT NULL,
                                          client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
                                          client_secret varchar(200) DEFAULT NULL,
                                          client_secret_expires_at timestamp DEFAULT NULL,
                                          client_name varchar(200) NOT NULL,
                                          client_authentication_methods varchar(1000) NOT NULL,
                                          authorization_grant_types varchar(1000) NOT NULL,
                                          redirect_uris varchar(1000) DEFAULT NULL,
                                          post_logout_redirect_uris varchar(1000) DEFAULT NULL,
                                          scopes varchar(1000) NOT NULL,
                                          client_settings varchar(2000) NOT NULL,
                                          token_settings varchar(2000) NOT NULL,
                                          PRIMARY KEY (id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authorization server를 띄우기 위해서는 필수적으로 필요한 테이블이 존재합니다. 위 테이블은 &lt;b&gt;org/springframework/security/oauth2/server/authorization/&lt;/b&gt; 패키지에서 해당 파일을 제공해주고 있으므로 그대로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AuthorizationServerConfig&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적인 AuthorizationServerConfig를 세팅할 차례입니다. 해당 클래스에는 세팅 코드가 많이 때문에 하나씩 쪼개서 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity // (debug = true)
class AuthorizationServerConfig(
    private val oAuthClientProperties: OAuthClientProperties,
    private val jwkSourceProperties: JWKSourceProperties,
    private val jdbcTemplate: JdbcTemplate,
) {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    fun authorizationServerSecurityFilterChain(
        http: HttpSecurity,
    ): SecurityFilterChain {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
        http.getConfigurer(OAuth2AuthorizationServerConfigurer::class.java)
//            .oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0

        http
            // Redirect to the login page when not authenticated from the
            // authorization endpoint
            .exceptionHandling { exceptions -&amp;gt;
                exceptions
                    .defaultAuthenticationEntryPointFor(
                        LoginUrlAuthenticationEntryPoint(&quot;/login&quot;),
                        MediaTypeRequestMatcher(MediaType.TEXT_HTML),
                    )
            } // Accept access tokens for User Info and/or Client Registration
            .oauth2ResourceServer { resourceServer -&amp;gt;
                resourceServer.jwt(Customizer.withDefaults())
            }

        return http.build()
    }

    @Bean
    @Order(2)
    @Throws(Exception::class)
    fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            // rest api csrf disable
            .csrf { csrf: CsrfConfigurer&amp;lt;HttpSecurity&amp;gt; -&amp;gt;
                csrf.ignoringRequestMatchers(&quot;/v1/**&quot;)
            }
            .authorizeHttpRequests { authorize -&amp;gt;
                authorize
                    .requestMatchers(&quot;/v1/**&quot;).permitAll()
                    .anyRequest().authenticated()
            }
            .formLogin(Customizer.withDefaults())
//            // only allow /oauth2 path to loginPage
            .exceptionHandling { exceptions -&amp;gt;
                exceptions
                    .defaultAuthenticationEntryPointFor(
                       JsonUnAuthorizedErrorEntryPoint(),
                    ) { request -&amp;gt; request.servletPath.startsWith(&quot;/oauth2&quot;).not() }
            }

        return http.build()
    }

    @Bean
    fun authorizationServerSettings(): AuthorizationServerSettings {
       return AuthorizationServerSettings.builder().issuer(oAuthClientProperties.issuerUrl).build()
    }

    @Bean
    fun jwtDecoder(jwkSource: JWKSource&amp;lt;SecurityContext&amp;gt;): JwtDecoder {
      return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource)
    }

   // ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class JsonUnAuthorizedErrorEntryPoint : AuthenticationEntryPoint {

  override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException?) {
    response.contentType = MediaType.APPLICATION_JSON_VALUE
    response.status = HttpStatus.UNAUTHORIZED.value()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-authorization-server/reference/getting-started.html#defining-required-components&quot;&gt;공식문서&lt;/a&gt;는 위와 같은 AuthorizationServer 세팅을 가이드하고 있습니다. 그리고 custom 하고자 한다면 위의 default 세팅에서 추가적으로 custom을 진행하면 됩니다. 데모에서는 기본적인 세팅에서 AuthorizationServer에서 유저 추가 등의 추가적인 API를 지원하기 위해 /v1 경로에 대한 설정을 추가했고, /oauth2 경로를 제외하고는 Login 페이지로 이동하지 않고 application/json 타입으로 401 응답을 내리는 EntryPoint를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthorizationServerSettings는 AuthorizationServer에서 제공하는 기본적인 default url 경로를 수정하는 기능을 제공합니다. 대부분 default로 사용하지만 issuerUrl 같은 경우는 실무 환경에서는 대부분 수정이 필요합니다. issuerUrl은 해당 서버의 실제 도메인을 명시하거나 외부에서 해당 서버를 바라보는 url을 명시해야 합니다. 해당 url은 jwt 토큰을 만드는 곳과 발급한 토큰을 타 서비스에서 검증하기 위해 해당 url 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthorizationServer가 OAuth2 ResourceServer 설정을 지정하는 이유는 AuthorizationServer도 User Info(OIDC 역할), ClientRegistration에 대한 정보(Resource)를 관리하고 있으므로 OAuth2 ResourceServer에 해당합니다. 따라서 Jwt 토큰을 해석할 수 있는 JwtDecoder도 빈으로 등록해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity // (debug = true)
class AuthorizationServerConfig() {
    // 생략

    @Bean
    fun oAuth2AuthorizationService(): OAuth2AuthorizationService {
        return JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository())
    }

    @Bean
    fun oAuth2AuthorizationConsentService(): OAuth2AuthorizationConsentService {
        return JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository())
    }

    @Bean
    fun registeredClientRepository(): RegisteredClientRepository {
      val client = getRegisteredClientCodeAndRefreshType(
        oAuthClientProperties.client,
        // RESOURCE에 대한 동의 선택지 추가
        // 예를 들면, 소셜 로그인 시에 선택하는 동의 내용들
        setOf(Scope.RESOURCE), 
      )

      return JdbcRegisteredClientRepository(jdbcTemplate).also { repository -&amp;gt;
        saveRegisteredClient(repository, client)
      }
    }
    // ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RegisteredClientRepository : Authorization Server에서 허용하는 Client는 RegisteredClient 클래스로 관리되는데 해당 클래스를 저장, 조회 등의 관리를 하는 클래스&lt;/li&gt;
&lt;li&gt;OAuth2 AuthorizationService : Client에 부여된 인가 상태는 OAuth2Authorization 클래스로 관리되는데 해당 클래스를 저장, 조회 등의 관리를 하는 클래스&lt;/li&gt;
&lt;li&gt;OAuth2 AuthorizationConsentService : Client 권한 부여에 대한 동의는 OAuth2AuthorizationConsent 클래스로 관리되는데 해당 클래스를 저장, 조회 등의 관리를 하는 클래스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 클래스 모두 별도로 등록하지 않으면 InMemory 구현체가 사용됩니다. 테스트 환경에서는 문제가 없으나 실무에서는 해당 클래스를 InMemory 구현체를 사용할 경우 OOM 발생 가능성이 있으므로 반드시 Jdbc 구현체를 사용해야 합니다. 해당 객체의 DB Schema는 앞서 schema.sql에서 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity // (debug = true)
class AuthorizationServerConfig {
    // 생략
    private fun getRegisteredClientCodeAndRefreshType(
        oAuthClient: OAuthClient,
        scopes: Set&amp;lt;Scope&amp;gt;,
    ): RegisteredClient {
        return baseRegisteredClientBuilder(oAuthClient, scopes)
            // Authorization Code Grant Type 지정
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            // 권한부여 승인 코드를 전달할 redirectUri
            .redirectUri(oAuthClient.redirectUri)
            .build()
    }

    private fun baseRegisteredClientBuilder(oAuthClient: OAuthClient, scopes: Set&amp;lt;Scope&amp;gt;): RegisteredClient.Builder {
        val clientBuilder = RegisteredClient.withId(oAuthClient.id) // RegisteredClient 객체 id값
            .clientId(oAuthClient.clientId) // 클라이언트 식별자
            .clientSecret(passwordEncoder().encode(oAuthClient.clientSecret)) // 클라이언트 비밀값
            .clientIdIssuedAt(Instant.now()) // 클라이언트 식별자가 발급된 시간
            .clientSecretExpiresAt(null) // 클라이언트 비밀값 만료 무제한
            // 클라이언트가 토큰을 발급받기 위해 요청을 보낼 때 client_id, client_secret을 받는 방식을 지정 -&amp;gt; POST 요청의 본문(body)에 포함하여 보내는 방식
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
            // scope를 추가한 경우, 동의 여부를 물을 것인지 지정
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            // 토큰 정보 지정
            .tokenSettings(
                TokenSettings.builder()
                    .accessTokenTimeToLive(oAuthClientProperties.accessTokenTtl)
                    .refreshTokenTimeToLive(oAuthClientProperties.refreshTokenTtl)
                    .reuseRefreshTokens(false) // 재발급할때 refresh토큰도 재발급 할지 여부
                    .build(),
            )

        // scope 추가
        if (scopes.isNotEmpty()) {
            clientBuilder.scopes {
                it.addAll(scopes.map { it.name })
            }
        }

        return clientBuilder
    }

    private fun saveRegisteredClient(
      repository: JdbcRegisteredClientRepository,
      client: RegisteredClient,
    ) {
      val foundClient = repository.findById(client.id)
      if (foundClient == null || foundClient != client) {
        repository.save(client)
      }
    }
    // ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에 대한 설명은 주석으로 대체하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity // (debug = true)
class AuthorizationServerConfig {

    // authorizationServer에서는 다른 서버들에게 jwk set = 암호화 정보를 제공해야 한다.
    @Bean
    fun jwkSource(): JWKSource&amp;lt;SecurityContext&amp;gt; {
        val rsaKey = generateRSAKey(jwkSourceProperties)
        val jwkSet = JWKSet(rsaKey)
        jwkSourceProperties.destroy()
        return ImmutableJWKSet(jwkSet)
    }

    // instance를 여러개 띄우게 된다면 만들어진 keyPair을 공유할 수 있도록 yml에 암호화해서 넣어두고 주입받는 형식으로 사용해야 한다.
    private fun generateRSAKey(jwkSourceProperties: JWKSourceProperties): RSAKey {
        val kf = KeyFactory.getInstance(RSA)

        val decodePublicKey = Base64.decodeBase64(String(jwkSourceProperties.publicKey))
        val x509EncodedKeySpec = X509EncodedKeySpec(decodePublicKey)
        val publicKey = kf.generatePublic(x509EncodedKeySpec) as RSAPublicKey

        val decodePrivateKey = Base64.decodeBase64(String(jwkSourceProperties.privateKey))
        val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(decodePrivateKey)
        val privateKey = kf.generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey

        return RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(jwkSourceProperties.kid)
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwkSource는 Jwt토큰에 서명할 때 사용됩니다. 해당 키값들은 ResourceServer들이 application이 뜰 때, AuthorizationServer의 /oauth2/jwks 경로를 호출하여 jwt 토큰 검증을 위한 공개키를 얻어 유효성 검사에 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity // (debug = true)
class AuthorizationServerConfig {
    @Bean
    fun userDetailsService(userRepository: UserRepository): UserDetailsService {
        return CustomUserDetailsService(userRepository)
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }
}&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;AuthorizationServer의 formLogin의 사용자 검증을 위해 등록해야 하는 기본 Security 세팅입니다. CustomUserDetailsService는 데모 코드를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity // (debug = true)
class AuthorizationServerConfig {
    @Bean
    fun tokenGenerator(userRepository: UserRepository): OAuth2TokenGenerator&amp;lt;*&amp;gt; {
        return DelegatingOAuth2TokenGenerator(
            JwtGenerator(NimbusJwtEncoder(jwkSource()), userRepository),
            OAuth2AccessTokenGenerator(),
            OAuth2RefreshTokenGenerator(),
        )
    }
}&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;tokenGenerator의 경우 Jwt를 제외하고는 기본값을 사용하고 JwtGenerator만 커스텀해서 세팅했습니다.&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;JwtGenerator&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 JwtGenerator는 빈으로 등록했고, 해당 클래스를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class JwtGenerator(
    private val jwtEncoder: JwtEncoder,
    private val userRepository: UserRepository,
) : OAuth2TokenGenerator&amp;lt;Jwt&amp;gt; {

    override fun generate(context: OAuth2TokenContext): Jwt? {
        if (!isValidContext(context)) {
            return null
        }

        // authorization 타입은 OAuth2Authorization
        val authorization = context.authorization!! 
        val loginId = authorization.principalName
        val user = userRepository.findByLoginId(loginId)
            ?: throw RuntimeException(&quot;User not found by loginId. $loginId&quot;)

        val issuedAt = Instant.now()
        val claimsBuilder = JwtClaimsSet.builder()
            .id(UUID.randomUUID().toString())
            .issuer(context.authorizationServerContext.issuer)
            .audience(listOf(user.id.toString()))
            .claim(&quot;userType&quot;, user.type.name)
            .claim(&quot;userRole&quot;, user.role.name)
            .issuedAt(issuedAt)
            .notBefore(issuedAt)
            .expiresAt(issuedAt.plus(context.registeredClient.tokenSettings.accessTokenTimeToLive))

      // 앞서 등록한 Client는 Scope로는 Resource만 담아두었습니다.
        if (context.authorizedScopes.isNullOrEmpty().not()) {
            claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.authorizedScopes)
        }

        return jwtEncoder.encode(
            JwtEncoderParameters.from(
                JwsHeader.with(SignatureAlgorithm.RS256).build(),
                claimsBuilder.build(),
            ),
        )
    }

    /**
     * @see org.springframework.security.oauth2.server.authorization.token.JwtGenerator
     */
    private fun isValidContext(context: OAuth2TokenContext): Boolean {
        val tokenType = context.tokenType ?: return false
        val isAccessToken = tokenType == OAuth2TokenType.ACCESS_TOKEN
        val isIdToken = tokenType.value == OidcParameterNames.ID_TOKEN

        if (!isAccessToken &amp;amp;&amp;amp; !isIdToken) {
            return false
        }
        if (isAccessToken &amp;amp;&amp;amp; context.registeredClient.tokenSettings.accessTokenFormat != OAuth2TokenFormat.SELF_CONTAINED) {
            return false
        }

        return true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 제공되는 org.springframework.security.oauth2.server.authorization.token.JwtGenerator를 참고하여 필요한 값들을 채워 넣어서 해당 클래스를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Client 서버 구축하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client의 경우, 토큰 발급에 대한 로직만 설명하겠습니다. 토큰 재발급, 삭제, 검증에 대한 로직은 데모 코드를 참고 바랍니다.&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;build.gradle.kts&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-validation&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client-Backend의 경우 단순한 web 의존성만 추가합니다.&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;Login redirect 로직&lt;/h4&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;1047&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byghLL/btsIqxVlQnX/msMktoV7z9ZWl4bTXUw6mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byghLL/btsIqxVlQnX/msMktoV7z9ZWl4bTXUw6mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byghLL/btsIqxVlQnX/msMktoV7z9ZWl4bTXUw6mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyghLL%2FbtsIqxVlQnX%2FmsMktoV7z9ZWl4bTXUw6mk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;655&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/v1/oauth2&quot;)
@RestController
class OAuthController(
    private val oAuthService: OAuthService,
    private val oAuthProperties: OAuthProperties,
) {

    @GetMapping(&quot;/login/{type}&quot;)
    fun login(@PathVariable type: String): RedirectView {
        return RedirectView(oAuthService.getOAuthLoginUrl(type))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class OAuthService(
    private val oAuthProperties: OAuthProperties,
    private val restTemplate: RestTemplate,
) {

    fun getOAuthLoginUrl(type: String): String {
        return generateOAuthLoginUrl(getOAuthClient(type))
    }

    private fun generateOAuthLoginUrl(client: OAuthClient): String {
        return StringBuilder(client.baseUrl)
            .append(&quot;/oauth2/authorize&quot;)
            .append(&quot;?response_type=code&quot;)
            .append(&quot;&amp;amp;client_id=${client.clientId}&quot;)
            .append(&quot;&amp;amp;state=${UUID.randomUUID()}&quot;)
            .append(&quot;&amp;amp;scope=RESOURCE&quot;)
            .toString()
    }
}&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;로그인 요청이 들어온 경우, Authorization Server의 토큰 발급 Url로 리다이렉트 시키는 로직입니다. 리다이렉트 되어 Authorization server가 요청을 받으면 인증이 되어있지 않은 경우 로그인 페이지로 이동하고 인증이 되어있다면 토큰 발급 로직이 진행되어 redirectUrl로 callback을 보내게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;state는 optional 한 값이나 보내게 되면 csrf 공격을 방지할 수 있습니다. 초기에 보낸 로그인 요청에서 state값을 보낸 경우, authorizationServer에 저장되고 콜백을 보낼 때도 state값을 queryString으로 붙여서 보내줍니다. 그리고 콜백에서 다시 AuthorizationServer로 토큰을 요청할 때, 반드시 state값을 보내야만 AuthorizationServer에서 state값을 비교하여 클라이언트의 요청이 변조되지 않았는지 확인합니다. 만약 로그인 요청 시에는 state값을 보냈으나 callback에서 다시 authorizationServer를 호출할 때 state값을 보내지 않으면 토큰이 발급되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;콜백 수신&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1047&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckJtkf/btsIrKsFQYE/si8siMdlk9ECptWKPp6HRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckJtkf/btsIrKsFQYE/si8siMdlk9ECptWKPp6HRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckJtkf/btsIrKsFQYE/si8siMdlk9ECptWKPp6HRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckJtkf%2FbtsIrKsFQYE%2Fsi8siMdlk9ECptWKPp6HRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;655&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/v1/oauth2&quot;)
@RestController
class OAuthController(
  private val oAuthService: OAuthService,
  private val oAuthProperties: OAuthProperties,
) {
    @GetMapping(&quot;/{type}/callback/authorization-code&quot;)
    fun issueTokenCallback(
        @PathVariable type: String,
        @RequestParam(&quot;code&quot;) code: String,
        @RequestParam(name = &quot;state&quot;, required = false) state: String?,
    ): ResponseEntity&amp;lt;Unit&amp;gt; {
        val token = oAuthService.issueToken(type, code, state)

        return ResponseEntity.ok()
            .headers(generateTokenCookieHeaders(token, type)) // 응답받은 토큰을 쿠키로 말아서 리턴
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class OAuthService(
  private val oAuthProperties: OAuthProperties,
  private val restTemplate: RestTemplate,
) {
    fun issueToken(type: String, code: String, state: String?): TokenResponse {
        val client = getOAuthClient(type)
        val request = generateIssueTokenRequest(client, code, state)
        val baseUrl = &quot;${client.baseUrl}/oauth2/token&quot;

        return callOrThrow(baseUrl, request)
    }

    private fun generateIssueTokenRequest(client: OAuthClient, code: String, state: String?): HttpEntity&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; {
      val params: MultiValueMap&amp;lt;String, String&amp;gt; = LinkedMultiValueMap()
      state?.let { params[STATE] = state }
      params[CODE] = code
      params[GRANT_TYPE] = AUTHORIZATION_CODE
      params[CLIENT_ID] = client.clientId
      params[CLIENT_SECRET] = client.clientSecret

      val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_FORM_URLENCODED }
      return HttpEntity(params, headers)
    }  
}&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;콜백을 받을 때 queryString으로 받은 code와 state(optional)을 그대로 request에 담아주고 client_secret과 grant_type도 추가해서 요청을 보내면 AuthorizationServer에서 토큰을 응답해 줍니다. 위 컨트롤러에서는 해당 토큰을 쿠키로 말아서 리턴했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Resource Server 구축하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle.kts&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-validation&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-oauth2-resource-server&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resourece 서버 의존성을 추가합니다.&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;application.yml&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  application:
    name: hello-resource-server
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:9000&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;resource 서버는 Jwt 토큰 검증을 위해 AuthorizationServer로부터 공개키를 가져와야 합니다. 따라서 application.yml에는 issuer-uri를 명시해야 합니다. 해당 issuer-uri는 jwt 토큰의 issuer claim과 비교에도 사용됩니다.&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;SecurityConfig&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
class SecurityConfig(
    private val oAuthProperties: OAuthProperties,
) {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.csrf { csrf -&amp;gt;
            csrf.disable()
        }
            .authorizeHttpRequests { authorize -&amp;gt;
                authorize
                    .anyRequest().hasAuthority(&quot;SCOPE_RESOURCE&quot;)
            }
            .oauth2ResourceServer { resourceServer -&amp;gt;
                resourceServer.jwt(Customizer.withDefaults())
            }


        http.addFilterBefore(OAuth2CookieTokenFilter(oAuthProperties), UsernamePasswordAuthenticationFilter::class.java)
        http.addFilterAfter(
            JWTAuthenticationConvertFilter(),
            BearerTokenAuthenticationFilter::class.java,
        )
        return http.build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-sansboot&quot;&gt;공식문서에&lt;/a&gt; 따른 기본 세팅은 위와 같습니다. resourceServer에 대한 설정과 jwt에 대한 설정이 있습니다. 해당 Resource 서버는 AuthorizationServer에서 Scope를 Resource로 받은 토큰에 대해서 허용하기 위해 hasAuthority에 RESOURCE 스코프를 지정했습니다. 기타 부가적인 Security 권한 체크는 데모 코드를 참고 바랍니다.&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;OAuth2 CookieTokenFilter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class OAuth2CookieTokenFilter(
    private val oAuthProperties: OAuthProperties,
) : OncePerRequestFilter() {

    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {

        val accessToken = WebUtils.getCookie(request, oAuthProperties.cookieName.accessCookieName)?.value
        if (accessToken.isNullOrBlank()) {
            return filterChain.doFilter(request, response)
        }

        val wrappedRequest: HttpServletRequestWrapper = object : HttpServletRequestWrapper(request) {
            override fun getHeader(name: String): String? {
                return if (name == HttpHeaders.AUTHORIZATION) {
                    &quot;$BEARER $accessToken&quot;
                } else {
                    super.getHeader(name)
                }
            }
        }
        return filterChain.doFilter(wrappedRequest, response)
    }

    companion object {
        private const val BEARER = &quot;Bearer&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 client-backend에서는 토큰을 쿠키로 말아서 front-end로 리턴했고 front-end는 쿠키를 resource-server로 전달하기 때문에 쿠키를 풀어서 bearer 헤더로 넘겨주는 작업이 필요한데 해당 작업을 하는 필터입니다. request에 대한 헤더는 수정이 불가능하기 때문에 한번 감싼 wrapper를 사용합니다. Bearer로 들어간 토큰은 ResourceServer에서 제공하는 BearerTokenAuthenticationFilter에서 검증이 이뤄지고 검증이 성공한다면 JwtAuthenticationToken 클래스로 SecurityContext의 authentication으로 저장됩니다.&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;JWTAuthenticationConvertFilter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class JWTAuthenticationConvertFilter : OncePerRequestFilter() {
    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
        val authentication = SecurityContextHolder.getContext().authentication
        if (authentication !is JwtAuthenticationToken || authentication.principal !is Jwt) {
            return filterChain.doFilter(request, response)
        }

        val user = generateUser(authentication)
        SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(user, null, user.authorities)
            .apply {
                details = WebAuthenticationDetailsSource().buildDetails(request)
            }

        return filterChain.doFilter(request, response)
    }

    private fun generateUser(authentication: JwtAuthenticationToken): User {

        val claims = (authentication.principal as Jwt).claims
        return User(
            id = (claims[&quot;aud&quot;] as List&amp;lt;String&amp;gt;).first().toLong(),
            type = claims[&quot;userType&quot;].toString(),
            role = claims[&quot;userRole&quot;].toString(),
            authorities = authentication.authorities
        )
    }
}&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;BearerTokenAuthenticationFilter에서 JWT 토큰을 검증하고 SecurityContext에 JWTAuthentication을 넣어주므로 여기서는 해당 Authentication을 꺼내서 사용하기 편한 객체로 변환해서 다시 SecurityContextHolder에 넣어주는 작업을 하는 필터입니다.&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;ResourceController&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class ResourceController {

    // @PreAuthorize(&quot;hasPermission(#id, 'RESOURCE', 'READ')&quot;)
    @PreAuthorize(&quot;hasRole('RAED_ALL_TITLE') || hasUserType('OPERATOR')&quot;)
    @GetMapping(&quot;/resource/{id}&quot;)
    fun getResource(@AuthenticationPrincipal user: User): String {
        return &quot;resource&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 컨트롤러에서는 SCOPE_RESOURCE를 포함된 jwt 토큰만을 허용하고 해당 토큰에 있는 userType과 userRole을 통해 추가 검증이 가능해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트러블 슈팅 (Authorization server)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RSAKey&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private fun generateRSAKey(jwkSourceProperties: JWKSourceProperties): RSAKey {
      val kf = KeyFactory.getInstance(&quot;RSA&quot;)

      val decodePublicKey = Base64.decodeBase64(String(jwkSourceProperties.publicKey))
      val x509EncodedKeySpec = X509EncodedKeySpec(decodePublicKey)
      val publicKey = kf.generatePublic(x509EncodedKeySpec) as RSAPublicKey

      val decodePrivateKey = Base64.decodeBase64(String(jwkSourceProperties.privateKey))
      val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(decodePrivateKey)
      val privateKey = kf.generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey

      return RSAKey.Builder(publicKey)
          .privateKey(privateKey)
          .keyID(jwkSourceProperties.kid)
          .build()
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthorizationServer에서 jwkSource를 만들 때 사용되는 키값은 properties를 통해 주입받아서 사용했습니다. Local환경에서는 인스턴스가 1개이기 때문에 주입받지 않고 바로 key값을 만들어서 사용해도 이슈가 없으나 리얼 환경에 배포되면서 인스턴스가 여러 개가 생기게 되는 경우 이슈가 발생합니다. 따라서 해당 키는 미리 만들어 properties로 주입받아야 합니다. 해당 키는 아래와 같이 찍힌 값으로 properties에 명시하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class JwkSourceTest {

    @Test
    fun createJwkSourceKey() {
        val keyPairGenerator = KeyPairGenerator.getInstance(&quot;RSA&quot;)
        keyPairGenerator.initialize(2048)
        val keyPair = keyPairGenerator.generateKeyPair()

        println(Base64.getEncoder().encodeToString(keyPair.public.encoded))
        println(Base64.getEncoder().encodeToString(keyPair.private.encoded))
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;session&lt;/h3&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;1047&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boFvo6/btsIqa0pZT4/vrjd7UG0U2XhZWtpRuJNS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boFvo6/btsIqa0pZT4/vrjd7UG0U2XhZWtpRuJNS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boFvo6/btsIqa0pZT4/vrjd7UG0U2XhZWtpRuJNS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboFvo6%2FbtsIqa0pZT4%2Fvrjd7UG0U2XhZWtpRuJNS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;655&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;655&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;Client에서 Authorization Server로 토큰을 요청할 때, 기본적으로 /oauth2/authorization?.. url로 요청을 보내게 됩니다. 하지만 AuthorizationServer에 인증이 되어있지 않은 경우 /login 경로로 리다이렉트 되게 되고 해당 페이지에서 로그인을 성공하면 인증 정보는 세션에 저장되고 다시 원래 요청으로 리다이렉트 됩니다. 로컬에는 인스턴스가 1개이기 때문에 이슈가 없으나 실무에서는 인스턴스가 여러 개이기 때문에 리다이렉트 된 요청이 다른 인스턴스로 들어가게 되면 세션을 찾지 못하는 이슈가 발생할 수 있습니다. 따라서 모든 인스턴스가 공유할 수 있는 세션 저장소로 redis를 채택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// redis
implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis&quot;)
implementation(&quot;org.springframework.session:spring-session-data-redis&quot;)
implementation(&quot;org.apache.commons:commons-pool2:2.12.0&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
server:
  port: 9000
  servlet:
    session:
      cookie:
        path: /
        name: HELLOSESSION
        domain: 127.0.0.1 # oauth server domain
        http-only: true
      timeout: 30m # oauth 로그인 후 리다이렉트 세션 유지

spring:
  application:
    name: hello-authorization-server
  session:
    store-type: redis
    redis:
      namespace: oauth:hello

  data:
    redis:
      url: localhost
      port: 6379
      connect-timeout: 3000
      timeout: 3000

# 생략..&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class RedisConfig(
    private val redisProperties: RedisProperties,
) {

    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        return LettuceConnectionFactory(
            RedisStandaloneConfiguration(redisProperties.url, redisProperties.port),
            lettucePoolingClientConfiguration(),
        )
        // 아래는 클러스터 설정
//        return LettuceConnectionFactory(RedisClusterConfiguration(listOf(redisProperties.url)), lettuceClientConfiguration)
    }

    private fun lettucePoolingClientConfiguration(): LettucePoolingClientConfiguration {
        val topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enableAllAdaptiveRefreshTriggers()
            .enablePeriodicRefresh(Duration.ofSeconds(30)) // 기본값이 disabled이므로 설정 필수, 권장 주기 30초
            .dynamicRefreshSources(true) // 기본값이 true이므로 설정하지 않아도 되지만 false로 변경은 금지
            .build()


        val timeoutOptions = TimeoutOptions.builder()
            .timeoutCommands()
            .fixedTimeout(redisProperties.timeout) // 사용 용도에 따라 자유롭게 설정
            .build()

        val clientOptions = ClusterClientOptions.builder()
            .topologyRefreshOptions(topologyRefreshOptions)
            .timeoutOptions(timeoutOptions)
            .build()

        return LettucePoolingClientConfiguration.builder()
            .clientOptions(clientOptions)
            .poolConfig(GenericObjectPoolConfig&amp;lt;Any&amp;gt;())
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mixed Content&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 요청이 https로부터 왔으나 리다이렉트 되는 요청이 http인 경우에 발생합니다. 리얼에서는 보통 https를 사용하기 때문에 이슈가 없지만 로컬 테스트환경에서는 이런 경우가 있을 수 있습니다. 이 경우에는 application.yml에 아래 옵션을 추가해서 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
server:
  tomcat:
    remoteip:
      protocol-header: x-forwarded-proto&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-authorization-server/reference/getting-started.html&quot;&gt;Spring 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://guide.ncloud-docs.com/docs/b2bpls-oauth2&quot;&gt;Oauth2.0 개념 및 연동&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>oAuth2</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/92</guid>
      <comments>https://backtony.tistory.com/92#entry92comment</comments>
      <pubDate>Mon, 8 Jul 2024 00:50:18 +0900</pubDate>
    </item>
    <item>
      <title>멀티 프로젝트 헥사고날 아키텍처로 구축하기</title>
      <link>https://backtony.tistory.com/91</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는 &lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/etc/hexagonal/hexagonal-sample&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hexagonal 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1tC43/btsImd3Mmrn/rvwZ5QdMRlpRL9ap6FQzVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1tC43/btsImd3Mmrn/rvwZ5QdMRlpRL9ap6FQzVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1tC43/btsImd3Mmrn/rvwZ5QdMRlpRL9ap6FQzVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1tC43%2FbtsImd3Mmrn%2FrvwZ5QdMRlpRL9ap6FQzVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1072&quot; height=&quot;582&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처(Hexagonal Architecture)는 포트와 어댑터 아키텍처(Ports and Adapters Architecture)라고도 불리는 소프트웨어 아키텍처 중 하나로 주요 목표는 응용 프로그램의 비즈니스 로직을 외부 세계로부터 격리시켜 유연하고 테스트하기 쉬운 구조를 만드는 것입니다. 이를 위해 핵심 비즈니스 로직은 중앙의 도메인 영역에 위치하며, 입력과 출력을 처리하는 포트와 어댑터를 통해 외부와 소통합니다.&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;Adapter : 모든 외부 시스템과의 직접적인 상호작용을 담당
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) Controller, Kafka Listener, DB DAO&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Inbound &amp;amp; Outbound port : 각 서비스 비즈니스 로직에 맞게 정의된 인터페이스&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;인바운드 어댑터 -&amp;gt; 인바운드 포트 -&amp;gt; 비즈니스 로직 -&amp;gt; 아웃바운드 포트 -&amp;gt; 아웃바운드 어댑터&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부의 로직은 오직 외부를 통해서만 접근이 가능한 콘셉트로 외부 서비스와의 상호 작용을 담당하는 Adapter는 비즈니스 로직과의 작업을 정의한 포트(인터페이스)랑만 서로 통신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Layered 아키텍처에서 Hexagonal 아키텍처로&lt;/h3&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;1234&quot; data-origin-height=&quot;1554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diNVGg/btsIm3M6LqZ/bix4R6q4wRy6woc0kK5L30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diNVGg/btsIm3M6LqZ/bix4R6q4wRy6woc0kK5L30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diNVGg/btsIm3M6LqZ/bix4R6q4wRy6woc0kK5L30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiNVGg%2FbtsIm3M6LqZ%2Fbix4R6q4wRy6woc0kK5L30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;630&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;1554&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;레이어드 아키텍처는 비즈니스 레이어가 인프라 레이어에 의존하여 강결합되는 구조를 가지고 있습니다. JPA로 예를 들면, PersistenceInterface는 JPA interface가 되고, 이에 대한 Adapter로는 SimpleJpaRepository가 될 수도 있고 QueryDsl을 사용한다면 추가적인 customImpl이 Adapter가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 역전&lt;/b&gt;&lt;br /&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;1268&quot; data-origin-height=&quot;1608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNP5NA/btsIlDIKuya/11y3FA8JY1zkokjk6ZqVYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNP5NA/btsIlDIKuya/11y3FA8JY1zkokjk6ZqVYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNP5NA/btsIlDIKuya/11y3FA8JY1zkokjk6ZqVYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNP5NA%2FbtsIlDIKuya%2F11y3FA8JY1zkokjk6ZqVYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;634&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;1608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 역전 원칙을 아웃고잉 어댑터에 적용하여 핵심 도메인 부분은 다른 레이어에 의존하지 않게 되어 독자적으로 개발 및 배포가 가능해졌습니다. 덕분에 테스트 용이성 또한 확보되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터페이스 분리&lt;/b&gt;&lt;br /&gt;보통 비즈니스 레이어의 서비스는 하나 이상의 유즈케이스를 구현하고 있습니다. 즉, UI 레이어에서 비즈니스 레이어의 서비스를 호출할 때, 어떤 유즈케이스를 사용해야 할지 명확하지 않은 경우가 발생할 수 있습니다. 또한, 위 그림의 appService를 그대로 사용하게 되면 여러 유즈케이스들의 불필요한 부분들을 모두 의존하는 상황이 발생합니다. 헥사고날 아키텍처에서는 다음과 같이 인터페이스를 분리합니다.&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;1226&quot; data-origin-height=&quot;1740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R90iA/btsIlP9UaPb/EmHiue4D9Df9GiSzJjCUW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R90iA/btsIlP9UaPb/EmHiue4D9Df9GiSzJjCUW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R90iA/btsIlP9UaPb/EmHiue4D9Df9GiSzJjCUW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR90iA%2FbtsIlP9UaPb%2FEmHiue4D9Df9GiSzJjCUW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;710&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;1740&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;인터페이스를 분리하고 이를 appService가 구현하게 하고 UI 레이어에서는 적절한 인커밍 포트를 사용하게 됩니다. 이를 통해 해당 기능과 관련 없는 다른 부분에 의존하지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처를 헥사고날 아키텍처로 변환하는 과정을 보면 다음과 같은 장점을 볼 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유지보수성 : 책임이 분리되어 있어 코드 이해와 수정이 용이합니다.&lt;/li&gt;
&lt;li&gt;유연성 : 포트와 어댑터를 사용함으로써, 다양한 변화에 대해 유연하게 대처할 수 있습니다.&lt;/li&gt;
&lt;li&gt;테스트 용이성 : 각 컴포넌트를 독립적으로 외부 의존성 없이 테스트할 수 있습니다.&lt;/li&gt;
&lt;li&gt;구조적으로 SOLID 원칙을 더욱 쉽게 적용 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처가 모든 상황에 적합한 것은 아닙니다. 헥사고날 아키텍처의 경우 기존 레이어드 아키텍처에 비해 코드량이 상당히 증가하며 처음 개발 이후 큰 비즈니스 로직의 변화가 존재하지 않는 프로젝트의 경우 오히려 레이어드 아키텍처가 더욱 안정적일 수 있습니다. 헥사고날 아키텍처는 보통 빠른 확장성과 유연성이 필연적으로 필요한 MSA 환경에서 적절한 아키텍처라고 표현하는 경우도 있으니 적절한 상황에 맞게 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티 프로젝트로 Hexagonal 아키텍처 구축하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 레포지토리로 안에 여러 개의 프로젝트를 만들고 각각의 프로젝트는 멀티 모듈로 구성하는 데모를 만들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 지식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 프로젝트 멀티 모듈을 구축하기 위해서는 사전 지식이 필요합니다.&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;implementation과 api&lt;/h4&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;implementation : implementation를 사용하면 해당 종속성은 현재 모듈 내부에서만 접근할 수 있습니다. 즉, 이 모듈을 의존하는 다른 모듈에서는 implementation으로 추가된 종속성에 직접 접근할 수 없습니다.&lt;/li&gt;
&lt;li&gt;api : 현재 모듈과 이 모듈을 의존하는 다른 모듈에서도 접근할 수 있도록 합니다. 즉, 현재 모듈이 api로 종속성을 추가하면, 이 모듈을 사용하는 모든 다른 모듈도 해당 종속성을 직접 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// A Module
public class A 

// B Module
implementation project(':A')

// C Module
implementation project(':B')

public class C {
  public void act() {
    new A() // compile error
  }
}&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;A 모듈에서 A라는 클래스를 제공한다고 했을때, B 모듈에서 A모듈을 implementation으로 의존성을 가져온다면 B모듈에서는 A 클래스를 사용할 수 있습니다. 이 상태에서 C 모듈에서 B 모듈을 implementation으로 의존성으로 가져온다면 C모듈에서는 A클래스를 사용할 수 없습니다. 하지만 B모듈에서 implementation이 아닌 api를 사용해서 A모듈의 의존성을 가져왔다면 C모듈에서도 A모듈에서 제공하는 기능을 사용할 수 있으므로 A 클래스를 사용할 수 있게 됩니다.&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;include와 includeBuild&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/composite_builds.html&quot;&gt;https://docs.gradle.org/current/userguide/composite_builds.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&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;include : 일반적으로 하나의 루트 프로젝트와 여러 서브 프로젝트로 구성된 구조에서 사용됩니다. include를 사용하면 서브 프로젝트들을 하나의 설정으로 결합하고, 이들 사이에 의존성을 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;includeBuild : 다른른 독립적인 빌드를 포함하는 데 사용됩니다. 보통 여러 개의 독립적으로 빌드 가능한 프로젝트를 하나의 빌드에 포함하고자 할 때 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 모듈의 경우 보통 include만 사용하지만 멀티 프로젝트를 만드는 경우, 타 프로젝트의 모듈을 가져오기 위해 includeBuild를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;├── common-library
│ ├── json
│ ├── settings.gradle.kts
│ └── build.gradle.kts
├── sample-service
│ ├── application
│ ├── build.gradle.kts
│ └── settings.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts&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;위와 같은 구조의 sample-service, common-library 프로젝트가 있을 때, common-library 프로젝트의 json 모듈을 sample-service 프로젝트의 application 모듈에서 사용하기 위해서 includeBuild를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// root 프로젝트의 settings.gradle.kts
includeBuild(&quot;common-library&quot;)
includeBuild(&quot;sample-service&quot;)

// sample-service의 settings.gradle.kts
rootProject.name = &quot;sample-service&quot;

includeBuild(&quot;../common-library&quot;)

// sample-service의 application 모듈의 build.gradle.kts
dependencies {
  // 패키지명:모듈명
  implementation(&quot;com.sample.hexagonal.common:json&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 구조로 의존성을 받아올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 구조&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;├── build-plugin
├── common-library
│ ├── exception
│ ├── json
│ └── utils
├── sample-service
│ ├── adapter
│ │ ├── inbound
│ │ │ ├── controller
│ │ │ └── listener
│ │ └── outbound
│ │     ├── producer
│ │     └── repository
│ ├── application
│ ├── domain
│ ├── infrastructure
│ │ ├── h2
│ │ └── mongo
│ ├── server
│ │ ├── api
│ │ └── consumer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;build-plugin : 여러 모듈에서 사용할 build-plugin을 관리하는 프로젝트&lt;/li&gt;
&lt;li&gt;common-library : 여러 모듈에서 공용으로 사용할 library를 관리하는 프로젝트&lt;/li&gt;
&lt;li&gt;sample-service : 서비스 프로젝트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;adapter.inbound : 외부 시스템과의 상호작용을 담당하는 Adapter 모듈로 외부에서 내부를 호출하는 역할 (controller, kafka-listener 모듈)&lt;/li&gt;
&lt;li&gt;adapter.outbound : 외부 시스템과의 상호작용을 담당하는 Adapter 모듈로 내부에서 외부를 호출하는 역할 (repository, kafka-producer 모듈)&lt;/li&gt;
&lt;li&gt;application : 비즈니스 모듈&lt;/li&gt;
&lt;li&gt;domain : 도메인 모듈&lt;/li&gt;
&lt;li&gt;infrastructure : outbound 모듈에서 사용하는 외부 인프라 (h2, mongo 모듈)&lt;/li&gt;
&lt;li&gt;server : 서버를 띄우기 위한 (api, consumer 모듈)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최상위 settings.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;includeBuild(&quot;build-plugin&quot;)
includeBuild(&quot;common-library&quot;)
includeBuild(&quot;sample-service&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위 settings.gradle.kts에는 composite build를 위해 includeBuild를 사용하여 각각의 프로젝트를 등록해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build-plugin 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build-plugin 프로젝트는 타 프로젝트의 모듈에서 사용할 공통적인 의존성을 관리하기 위한 목적의 프로젝트입니다. 공식문서에서 가이드하는 &lt;a href=&quot;https://docs.gradle.org/current/userguide/custom_plugins.html#sec:precompile_script_plugin&quot;&gt;Precompiled script plugin&lt;/a&gt; 방식을 사용하여 여러 프로젝트에서 공용으로 사용할 build-plugin을 만들겠습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;├── build.gradle.kts
├── settings.gradle.kts
└── src
    └── main
        └── kotlin
            ├── sample-kotlin-jvm.gradle.kts
            └── sample-springboot.gradle.kts&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;build-plugin 프로젝트의 tree구조는 위와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sample-kotlin-jvm.gradle.kts&lt;/b&gt;&lt;br /&gt;해당 파일은 spring이 아닌 단순 kotlin만을 사용하는 모듈을 위한 플러그인 파일입니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType

plugins {
    id(&quot;org.jlleitschuh.gradle.ktlint&quot;)
    id(&quot;org.jetbrains.kotlinx.kover&quot;)
    id(&quot;java-library&quot;)

    kotlin(&quot;jvm&quot;)
    kotlin(&quot;kapt&quot;)
}

repositories {
    mavenCentral()
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

dependencies {
    implementation(&quot;org.jetbrains.kotlin:kotlin-stdlib-jdk8&quot;)
    implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
    implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin&quot;)

    implementation(&quot;io.github.microutils:kotlin-logging-jvm:3.0.5&quot;)

    testImplementation(&quot;io.kotest:kotest-runner-junit5-jvm:5.8.0&quot;)
    testImplementation(&quot;io.kotest:kotest-assertions-core-jvm:5.8.0&quot;)
    testImplementation(&quot;io.kotest:kotest-framework-datatest:5.8.0&quot;)
    testImplementation(&quot;io.mockk:mockk:1.13.8&quot;)
}

tasks.withType&amp;lt;KotlinCompile&amp;gt; {
    kotlinOptions {
        freeCompilerArgs = listOf(&quot;-Xjsr305=strict&quot;)
        jvmTarget = &quot;17&quot;
    }
}

tasks.withType&amp;lt;Test&amp;gt; {
    useJUnitPlatform()
}&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;sample-springboot.gradle.kts&lt;/b&gt;&lt;br /&gt;해당 파일은 spring을 사용하는 모듈을 위한 플러그인 파일입니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    id(&quot;sample-kotlin-jvm&quot;) // 앞서 작성한 sample-kotlin-jvm.gradle.kts 파일을 플러그인으로 사용합니다.
    id(&quot;org.springframework.boot&quot;)
    id(&quot;io.spring.dependency-management&quot;)
    kotlin(&quot;plugin.spring&quot;)
}

dependencies {
    kapt(&quot;org.springframework.boot:spring-boot-configuration-processor&quot;)

    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
    testImplementation(&quot;io.kotest.extensions:kotest-extensions-spring:1.1.3&quot;)
    testImplementation(&quot;com.ninja-squad:springmockk:4.0.2&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 만들었던 sample-kotlin-jvm.gradle.kts 파일을 플러그인으로 사용했습니다. 이와 같은 방식으로 이제 앞으로 만들어낼 모듈들의 build.gradle.kts 파일에는 spring이 필요한 경우 &lt;b&gt;id(&quot;sample-springboot&quot;)&lt;/b&gt; 를 사용하고 kotlin에 대한 의존성만 필요할 경우 &lt;b&gt;id(&quot;sample-kotlin-jvm&quot;)을&lt;/b&gt; 사용해서 build.gradle.kts 파일을 더욱 간결하게 만들어낼 수 있습니다.&lt;/p&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;build.gradle.kts&lt;/b&gt;&lt;br /&gt;build.gradle.kts의 plugins 블록에는 원래는 플러그인의 버전 정보를 함께 명시해야 합니다. 하지만 convention 플러그인을 만들 때는 명시한 플러그인의 버전은 build.gradle의 dependency로 지정해줘야 합니다. 관련 내용은 &lt;a href=&quot;https://discuss.gradle.org/t/applying-a-plugin-version-inside-a-convention-plugin/42160&quot;&gt;forums&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
    gradlePluginPortal()
}

dependencies {
    // jvm
    implementation(&quot;org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20&quot;)
    implementation(&quot;org.jetbrains.kotlin.kapt:org.jetbrains.kotlin.kapt.gradle.plugin:1.9.20&quot;)
    implementation(&quot;org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin:0.7.5&quot;)
    implementation(&quot;org.jlleitschuh.gradle:ktlint-gradle:11.0.0&quot;)

    // spring
    implementation(&quot;org.jetbrains.kotlin:kotlin-allopen:1.9.20&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-gradle-plugin:3.2.0&quot;)
    implementation(&quot;io.spring.gradle:dependency-management-plugin:1.1.4&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;common-library 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;common-library는 생략하겠습니다. 자세한 코드는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/etc/hexagonal/hexagonal-sample&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sample-service 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;├── adapter
 ├── inbound
  └── controller
 └── outbound
     └── repository
├── application
├── domain
├── infrastructure
 └── h2
├── server
 └── api&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;데모 코드에는 여러 가지 모듈이 더 있지만 본 포스팅에서는 sample-service의 위 모듈에 대해서만 설명하겠습니다.&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;domain 모듈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    id(&quot;sample-kotlin-jvm&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain 모듈은 앞서 만들어둔 build-plugin 프로젝트의 kotlin 플러그인만 사용합니다.&lt;/p&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;Sample&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;class Sample(
    val id: String? = null,
    name: String,
    val createdAt: LocalDateTime = LocalDateTime.now(),
    updatedAt: LocalDateTime = LocalDateTime.now(),
) {
    // 생략
}&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;앞으로 나오는 코드는 이 Sample 클래스를 기반으로 작성됩니다.&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;application 모듈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;├── build.gradle.kts
└── src
    └── main
        └── kotlin
            └── com
                └── sample
                    └── hexagonal
                        └── sample
                            └── application
                                ├── port
                                │ ├── inbound
                                │ │ └── sample
                                │ │     ├── SampleDeleteInboundPort.kt
                                │ │     ├── SampleFindInboundPort.kt
                                │ │     └── SampleSaveInboundPort.kt
                                │ └── outbound
                                │     └── sample
                                │         ├── SampleDeleteOutboundPort.kt
                                │         ├── SampleFindOutboundPort.kt
                                │         └── SampleSaveOutboundPort.kt
                                ├── service
                                │ └── sample
                                │     └── SampleService.kt&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;application 모듈의 간략한 tree 구조는 위와 같습니다. inbound와 outbound는 앞서 언급했듯이 외부에서 내부로, 내부에서 외부로 나가는 통로 인터페이스입니다. application모듈의 service는 InboundPort 인터페이스를 구현하게 됩니다. 외부에서 내부로 들어오는 요청은 InboundPort를 통해 내부로 들어오고 내부에서 외부로 나가는 요청은 OutbountPort 인터페이스를 통해 나가게 됩니다.&lt;/p&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;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    id(&quot;sample-springboot&quot;)
}

dependencies {
    api(project(&quot;:domain&quot;))
    implementation(&quot;com.sample.hexagonal.common:kafka-producer&quot;)
    implementation(&quot;com.sample.hexagonal.common:exception&quot;)
    implementation(&quot;com.sample.hexagonal.common:kafka-producer&quot;)

    implementation(&quot;org.springframework.data:spring-data-commons&quot;)
    implementation(&quot;org.springframework:spring-context&quot;)
    implementation(&quot;org.springframework:spring-tx&quot;)
}

// 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application 모듈은 domain 모듈과 common-library 프로젝트에서 필요한 모듈을 의존성으로 받아서 사용합니다.&lt;/p&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;SampleService&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
class SampleService(
    private val sampleDeleteOutboundPort: SampleDeleteOutboundPort,
    private val sampleFindOutboundPort: SampleFindOutboundPort,
    private val sampleSaveOutboundPort: SampleSaveOutboundPort,
) : SampleDeleteInboundPort, SampleSaveInboundPort, SampleFindInboundPort {

    @Transactional
    override fun saveSample(name: String): Sample {
        return sampleSaveOutboundPort.save(
            Sample.create(name),
        )
    }
  // 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SampleService는 InbountPort 인터페이스를 구현하게 되고 외부로의 요청은 OutbountPort 인터페이스를 사용하여 처리합니다.&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;adapter.inbound.controller 모듈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데모 코드에는 여러 inbound 모듈이 있지만 controller 모듈만 살펴보겠습니다.&lt;/p&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;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    id(&quot;sample-springboot&quot;)
}

dependencies {
    implementation(project(&quot;:application&quot;))
    implementation(&quot;com.sample.hexagonal.common:json&quot;)
    implementation(&quot;com.sample.hexagonal.common:utils&quot;)

    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
}

// 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application 모듈과 common-library 프로젝트에서 필요한 모듈을 의존성으로 받습니다.&lt;/p&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;SampleController&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@RestController
class SampleController(
    private val sampleFindInboundPort: SampleFindInboundPort,
    private val sampleSaveInboundPort: SampleSaveInboundPort,
    private val sampleDeleteInboundPort: SampleDeleteInboundPort,
) {

    @PostMapping(&quot;/v1/sample&quot;)
    fun saveSample(@RequestBody sampleSaveRequest: SampleSaveRequest): SampleResponse {
        val sample = sampleSaveInboundPort.saveSample(sampleSaveRequest.name)
        return SampleResponse.from(sample)
    }
    // 생략
}&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;controller는 내부와 통신하기 위해 InbountPort 인터페이스를 사용하여 요청합니다.&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;adapter.outbound.repository 모듈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데모 코드에는 여러 outbound 모듈이 있지만 repository 모듈만 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    id(&quot;sample-springboot&quot;)
}

dependencies {
  implementation(project(&quot;:application&quot;))
  implementation(project(&quot;:infrastructure:h2&quot;))
//    implementation(project(&quot;:infrastructure:mongo&quot;))
}

// 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository모듈은 application과 infra의 h2모듈을 의존성으로 사용합니다.&lt;/p&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;SampleRepository&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
class SampleRepository(
    private val sampleDao: SampleEntityDao,
) : SampleDeleteOutboundPort, SampleFindOutboundPort, SampleSaveOutboundPort {

    override fun save(sample: Sample): Sample {
        return sampleDao.save(SampleMapper.mapDomainToEntity(sample))
            .let { SampleMapper.mapEntityToDomain(it) }
    }
    // 생략
}&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;Repository는 내부에서 외부의 요청에 사용되는 OutboundPort 인터페이스의 구현체를 생성합니다.&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;infrastructure.h2 모듈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    implementation(&quot;com.sample.hexagonal.common:utils&quot;)
    implementation(&quot;com.sample.hexagonal.common:json&quot;)

    api(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
    runtimeOnly(&quot;com.h2database:h2&quot;)
}

// 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2 모듈에서는 common-library에서 필요한 의존성을 추가합니다.&lt;/p&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;entity &amp;amp; dao&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
class SampleEntity(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    val name: String,

    @CreatedDate
    val createdAt: LocalDateTime,

    @LastModifiedDate
    val updatedAt: LocalDateTime,
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
interface SampleEntityDao : JpaRepository&amp;lt;SampleEntity, Long&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2 모듈에서는 Repository 모듈에서 사용할 인프라 기술들을 구현합니다.&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;server.api 모듈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
  id(&quot;sample-springboot&quot;)
}

dependencies {
    implementation(project(&quot;:domain&quot;))
    implementation(project(&quot;:application&quot;))
    implementation(project(&quot;:adapter:inbound:controller&quot;))
    implementation(project(&quot;:adapter:outbound:repository&quot;))
    implementation(project(&quot;:adapter:outbound:producer&quot;))
    implementation(project(&quot;:infrastructure:h2&quot;))

    implementation(&quot;com.sample.hexagonal.common:actuator&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)

    // 명시적으로 확인하기 위해서 추가
    implementation(&quot;com.sample.hexagonal.common:utils&quot;)
    implementation(&quot;com.sample.hexagonal.common:json&quot;)
    implementation(&quot;com.sample.hexagonal.common:kafka-producer&quot;)
    implementation(&quot;com.sample.hexagonal.common:exception&quot;)
}

// 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server-api 모듈은 서버를 띄우기 위한 껍데기 모듈입니다. 컴포넌트 스캔을 위해 해당 서버를 띄울 때 사용할 의존성들을 추가합니다. 외부 프로젝트 모듈의 경우 이미 내부 모듈의 의존성으로 들어가 있기 때문에 스프링 부트의 컴포넌트 스캔 메커니즘상 클래스패스에 존재하는 모든 패키지를 스캔할 수 있기 때문에 외부 프로젝트 모듈은 의존성으로 추가하지 않아도 됩니다. 하지만 외부 프로젝트 모듈을 추가하지 않으면 컴포넌트 스캔으로 지정할 basePackage를 누락할 가능성이 있기 때문에 명시적으로 의존성을 추가해 두고 basePackage를 지정할 때 참고할 수 있도록 해두었습니다.&lt;/p&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;SampleApplication&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@SpringBootApplication(
    scanBasePackages = [
        &quot;com.sample.hexagonal.sample.server.api&quot;,
        &quot;com.sample.hexagonal.sample.adapter&quot;,
        &quot;com.sample.hexagonal.sample.application&quot;,
        &quot;com.sample.hexagonal.sample.infrastructure&quot;,
        &quot;com.sample.hexagonal.common&quot;
    ],
)
class SampleApplication

fun main(args: Array&amp;lt;String&amp;gt;) {
    TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC))
    runApplication&amp;lt;SampleApplication&amp;gt;(*args)
}&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;외부 프로젝트의 의존성은 common-library 프로젝트의 모듈만 사용하므로 common을 추가했고 나머지는 내부 모듈의 패키지 경로를 추가했습니다. 현재 데모 코드상에서는 사실 &lt;b&gt;com.sample.hexagonal&lt;/b&gt; 만 명시하거나 &lt;b&gt;com.sample.hexagonal.sample&lt;/b&gt;으로 sample-service 패키지 경로를 입력해서 스캔 목록을 간소화할 수 있습니다. 하지만 위와 같이 한 이유는 만약 현재 api 서버에서 adapter.controller 모듈을 사용하나 adapter.controller.external 패키지만 사용하므로 해당 패키지만 추가하고 싶을 수 있고 이는 스캔 목록을 통해 컨트롤할 수 있다는 것을 보여드리기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165581&quot;&gt;Hexagonal Architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>ETC</category>
      <category>composite-build</category>
      <category>Hexagonal</category>
      <category>multi-module</category>
      <category>multi-project</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/91</guid>
      <comments>https://backtony.tistory.com/91#entry91comment</comments>
      <pubDate>Wed, 3 Jul 2024 21:58:50 +0900</pubDate>
    </item>
    <item>
      <title>Spring Data MongoDB</title>
      <link>https://backtony.tistory.com/90</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정히라는 &lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/database/mongodb/mongo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MongoDB란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB는 오픈소스 비관계형 데이터베이스 관리 시스템(DMBS)으로, 테이블과 행 대신 유연한 문서를 활용해 다양한 데이터 형식을 처리하고 저장합니다. NoSQL 데이터베이스 솔루션인 MongoDB는 관계형 데이터베이스 관리 시스템(RDBMS)을 필요로 하지 않으므로, 사용자가 다변량 데이터 유형을 손쉽게 저장하고 쿼리할 수 있는 탄력적인 데이터 저장 모델을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB 문서 또는 문서 컬렉션은 데이터의 기본 단위입니다. 해당 문서들은 이진 JSON(JavaScript 객체 표기법) 형식으로 지정되어 다양한 유형의 데이터를 저장할뿐 아니라, 여러 시스템 전반에 분산 처리될 수 있습니다. MongoDB는 동적 스키마 설계를 활용하므로 사용자는 독보적인 유연성을 확보해 데이터 레코드를 생성하고, MongoDB 집계를 통해 문서 컬렉션을 쿼리하며, 대량의 정보를 분석합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB는 4.0버전 이후부터 multi-document transaction을 지원합니다. Spring Data MongoDB를 사용할 경우, 연동하는 mongoDB는 replica로 구성되어야만 @Transactional을 사용할 수 있으므로 MonogoDB에 replica 구성이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;readPreference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read Preference 란 MongoDB의 Replica Set 설정 시 Primary와 Secondary 노드에 대한 작업 처리 분산에 대한 설정입니다.&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;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E4ISu/btsIgDJkcqE/WcSafD60ughkwlQV9R173k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E4ISu/btsIgDJkcqE/WcSafD60ughkwlQV9R173k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E4ISu/btsIgDJkcqE/WcSafD60ughkwlQV9R173k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE4ISu%2FbtsIgDJkcqE%2FWcSafD60ughkwlQV9R173k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;457&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Replica Set 구조를 살펴보면, MySQL의 Replication 과 동일한 방식입니다. 모든 명령은 기본적으로 Primary에서 처리하며 Secondary는 Primary에 기록된 데이터를 Sync 하며 유지하게 됩니다. 하지만, 이러한 구조는 Client가 Primary 하고만 데이터를 주고받기 때문에 MongoDB 서버로 총 3대를 사용함에도 작업 분산 효과를 누리지 못하며, 오로지 Secondary 노드는 데이터 백업 용도로만 사용할 수밖에 없게 됩니다.&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;830&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4H8vR/btsIjfM0oUY/BOLvyg2V6S9oxLaSEvlyv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4H8vR/btsIjfM0oUY/BOLvyg2V6S9oxLaSEvlyv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4H8vR/btsIjfM0oUY/BOLvyg2V6S9oxLaSEvlyv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4H8vR%2FbtsIjfM0oUY%2FBOLvyg2V6S9oxLaSEvlyv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;455&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;944&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;primary : default 값으로 모든 작업을 primary에서 수행합니다.&lt;/li&gt;
&lt;li&gt;primaryPreferred : primary를 default로 하되, primary가 불가 상태가 될 경우 secondary에서 read 처리&lt;/li&gt;
&lt;li&gt;secondary : secondary에서 모든 작업 수행&lt;/li&gt;
&lt;li&gt;secondaryPreferred : secondary를 default로 하되, secondary가 불가 상태 혹은 복제 지연이 발생하면 primary에서 수행&lt;/li&gt;
&lt;li&gt;nearest : 네트워크 레이턴시를 기준으로 가장 가까운 멤버에서 read 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;primary든 secondary든 preferred 옵션을 사용하는 것이 권장되나,&amp;nbsp;&lt;/b&gt;MongoDB에서 트랜잭션을 사용하는 경우, 트랜잭션 내의 모든 작업은 일관성을 보장하기 위해 primary 노드에서 실행되어야 합니다. 따라서 트랜잭션을 사용하는 경우 해당 옵션은 primary로 사용해야 합니다. &lt;a href=&quot;https://www.mongodb.com/community/forums/t/why-can-t-read-preference-be-secondary-in-a-transaction/204432&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mongo community&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;write concern&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write concern은 MongoDB가 Client의 요청으로 데이터를 기록할 때, 해당 요청에 대한 Response를 어느 시점에 주느냐에 대한 동작 방식을 지정하는 옵션입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQfjQK/btsIiiXYEKU/FKpe39FdiWyJJJDkqmQ1aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQfjQK/btsIiiXYEKU/FKpe39FdiWyJJJDkqmQ1aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQfjQK/btsIiiXYEKU/FKpe39FdiWyJJJDkqmQ1aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQfjQK%2FbtsIiiXYEKU%2FFKpe39FdiWyJJJDkqmQ1aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1426&quot; height=&quot;626&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB는 Client가 보낸 데이터를 Primary에 기록하고, 이에 대한 Response를 Client에게 보내게 됩니다. 이때, MongoDB를 레플리카 셋을 위 그림처럼 Primary 1대와 Secondary 2대로 구성하였을 경우, Client가 보낸 데이터의 Write 처리는 Primary에서만 먼저 처리하게 되며, 이후 Secondary로 변경된 데이터를 동기화 시키는 단계를 거칩니다. 이때 주의해야 할 점은 Primary와 Secondary 간 동기화 되는데 시간차가 있다는 점입니다. 만약 Client가 보낸 데이터를 Primary가 처리한 직후 Client 쪽으로 Response를 보내고 이후, Primary와 Secondary 간 동기화가 진행된다고 가정하면 Client가 Response를 받는 시점과 Primary에서 Secondary로 Sync 되는 타이밍 사이에는 데이터 일관성이 보장되지 않는 위험 구간이 존재하게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이 사이에 Primary에 장애가 발생 했다고 가정해 보면, 아직 최신 데이터를 Sync 하지 못한 Secondary 멤버가 Primary로 승격되고 Client는 이를 알아차리지 못한 채 이미 작업이 완료된 Response를 받았기 때문에 Client가 알고 있는 데이터와 DB의 데이터가 unmatch 되는 상횡이 발생되게 됩니다. 이러한 문제를 해결하기 위해 Client 쪽에 보내는 response 시점을 Primary와 Secondary가 동기화된 이후로 설정이 가능한데 이것이 바로 Write concern 설정의 핵심입니다.&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;1268&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpQJec/btsIih5PNji/K79lnqTXO9PdFIlaW6KuE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpQJec/btsIih5PNji/K79lnqTXO9PdFIlaW6KuE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpQJec/btsIih5PNji/K79lnqTXO9PdFIlaW6KuE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpQJec%2FbtsIih5PNji%2FK79lnqTXO9PdFIlaW6KuE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1268&quot; height=&quot;584&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;584&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;Write Concern을 설정하게 되면 Primary가 데이터 쓰기를 처리한 이후 바로 Client에게 Response를 보내는 것이 아니라 Secondary 쪽으로 데이터를 동기화 작업을 완료한 이후에 Client에게 Response를 보내게 되므로 Client와 Primary, Secondary 간에 데이터 일관성을 유지할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;w: &amp;lt;value&amp;gt;, j: &amp;lt;boolean&amp;gt;, wtimeout: &amp;lt;number&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;w : 레플리카 셋에 속한 멤버 중 지정된 수 만큼 멤버에게 데이터 쓰기가 완료되었는지 확인합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값은 1로 primary에만 확인하고 response합니다. 해당 값을 높이면 해당 숫자만큼의 멤버에서 write ack가 오면 client에 ack 응답을 보냅니다.&lt;/li&gt;
&lt;li&gt;majority : 과반수 이상의 멤버에서 write ack가 오면 client에 ack 응답을 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;j : 데이터 쓰기 작업이 디스크 상의 journal(디스크에 변경된 데이터, 인덱스를 적용하기 전 어떤 작업이 수행되었는지 로깅)에 기록된 후 완료로 판단하는 옵션입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;true : journal log까지 쓰고 ack(권장)&lt;/li&gt;
&lt;li&gt;false : memory에만 전달하고 ack&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;wtimeout : Primary에서 Secondary로 데이터 동기화 시 timeout 값을 설정하는 옵션입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;default 없음&lt;/li&gt;
&lt;li&gt;limit을 넘어가게 되면 실제로 데이터가 primary에 기록되었다고 해도 error를 리턴합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;default 값은 &lt;b&gt;w : 1, j : true&lt;/b&gt; 로 특별한 이슈가 없다면 그대로 사용을 권장합니다. 보통 majority 옵션을 사용한다면 read와 write concern 모두 majority를 같이 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Read Concern&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write Concern과 마찬가지로 어디까지 반영된 데이터를 읽을지 결정하는 옵션입니다.&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;local(권장)
&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;primary read 시 default이며, 5.0부터 secondary read 시에서 default입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;snapshot
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;multi document transaction 사용 시 default&lt;/li&gt;
&lt;li&gt;트랜잭션 시작 전의 상태를 snapshot으로 찍어서 사용하므로 현재 트랜잭션 시작 후 다른 트랜잭션에서 수정한 데이터는 현재 트랜잭션에서 실행되는 결과에 표시되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;linearizable
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 연산이 시작하는 시점 전에 과반수의 레플리카 멤버에게서 write 연산이 성공했다는 응답을 받은 데이터만을 읽습니다.&lt;/li&gt;
&lt;li&gt;읽기 연산이 write 연산과 동시에 발생할 경우, write 연산이 과반수의 레플리카 맴버에게 전파될 때까지 대기하고 난 후에 write 연산이 반영된 이후의 데이터를 응답합니다.&lt;/li&gt;
&lt;li&gt;multi document 조회에서는 사용 불가능하고 일부 aggregation pipeline을 사용 불가능($out 등)&lt;/li&gt;
&lt;li&gt;readPreference : primary와 함께 사용&lt;/li&gt;
&lt;li&gt;maxTimeMs 설정 필요 : 하나의 멤버라도 문제가 있으면 무한정 대기가 발생할 수 있으므로 지정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;majority
&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;primary-secondary-arbiter 구성에서 장애 유발 가능성이 있습니다. mongo server는 다른 member의 commit 상태 정보를 memory cache에 저장하고 반영되면 cache를 비우는데 이와 같은 구조에서, secondary가 장애 시 primary의 캐시가 비워지지 않고 이력을 계속 저장하게 되면서 primary cache pressure 발생 가능성이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;available
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4.4 버전까지 secondary read 시 default&lt;/li&gt;
&lt;li&gt;메타 데이터(config)를 확인하지 않아 샤딩의 경우 잘못된 데이터를 반환할 수 있으므로 샤딩에서 사용해선 안됩니다.&lt;/li&gt;
&lt;li&gt;사실상 어떤 서비스의 경우에도 이 옵션 사용을 하지 않는 것을 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Transaction Isolation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고디비는 SQL 표준에서 제시한 4가지 격리 수준과는 다른, 스냅샷 격리 수준(Snapshot isolation level)을 사용합니다. WiredTiger 스토리지 엔진은 Read Uncommitted, Read Committed, Snapshot 격리 수준을 지원하고, 몽고디비는 트랜잭션 격리 수준을 스냅샷으로 고정하여 스냅샷 격리 수준만 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WiredTiger(와 몽고디비) 트랜잭션에서의 스냅샷이란, 스냅샷 생성 시점에서 트랜잭션들의 상태를 캡처한 것입니다. 즉, 스냅샷에는 어떤 트랜잭션이 완료(커밋)되었고, 어떤 트랜잭션이 진행중인지, 해당 스냅샷을 가진 트랜잭션이 뭔지에 대한 정보를 담고 있습니다. WiredTiger는 트랜잭션이 시작할 때 스냅샷을 설정합니다. 이후에 모든 읽기 연산은 스냅샷에 있는 정보를 참고하여 문서의 어떤 버전이 읽을 수 있는 버전인지 확인하여 읽어옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 사용하지 않는 단일 문서 작업이나 특정 읽기 작업에서는 필요에 따라 다양한 readConcern 옵션을 활용하여 일관성을 유지할 수 있습니다. default값은 local은 커밋되지 않은 데이터도 읽을 수 있으므로 Read Uncommitted와 유사합니다. &lt;a href=&quot;https://www.mongodb.com/docs/manual/core/read-isolation-consistency-recency/&quot;&gt;공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;compound index 참고사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Equal, Sort, Range 순으로 compound key를 구성을 권장합니다. 다른 순서로도 가능하지만 Sort의 부하를 줄이는 것이 중요하므로 Sort 할 때 인덱스가 살아있다면 괜찮습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;docker-compose를 활용한 mongoDB replica 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key file은 replica set에 참여하는 mongod 인스턴스 간의 인증, 클라이언트 접속 시 access control에 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;key file 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# mongodb 키 생성
sudo openssl rand -base64 756 &amp;gt; ~/.ssh/replica-mongodb-test.key

# 권한 설정
sudo chmod 400 ~/.ssh/replica-mongodb-test.key

# 제대로 key가 생성되었는지 확인
cat ~/.ssh/replica-mongodb-test.key&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;docker-compose.yml&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;version: &quot;3.1&quot;
services:
  mongodb1:
    image: mongo
    container_name: mongo1
    hostname: mongo1
    restart: always
    ports:
      - &quot;27017:27017&quot;
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: root
    command: mongod --replSet rs0 --keyFile /etc/mongodb.key --bind_ip_all
    volumes:
      - ./db1:/data/db
      - ~/.ssh/replica-mongodb-test.key:/etc/mongodb.key

  mongodb2:
    image: mongo
    container_name: mongo2
    hostname: mongo2
    restart: always
    ports:
      - &quot;27018:27018&quot;
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: root
    command: mongod --replSet rs0 --keyFile /etc/mongodb.key --bind_ip_all
    volumes:
      - ./db2:/data/db
      - ~/.ssh/replica-mongodb-test.key:/etc/mongodb.key&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레플리카셋 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 실행
docker-compose up -d

# 정상적으로 올라왔는지 확인
docker ps -a

# container 접속
docker exec -it mongo1 /bin/bash

# root 계정 몽고 쉘 접속
mongosh -u root -p root

# admin 데이터베이스 사용
use admin

# replication 초기화
rs.initiate()

# mongo2 복제세트 추가
rs.add({_id: 1, host: &quot;mongo2:27017&quot;})

# 리플리카 셋 설정 정보 확인
rs.config()

# 리플리카 셋 상태정보 확인
rs.status()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;spring config 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-data-mongodb&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mongoDB 의존성을 추가해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  data:
    mongodb:
      uri : mongodb://root:root@localhost:27017/test?authSource=admin&amp;amp;connectTimeoutMS=10000
      database: test-mongo

logging:
  level:
    org.springframework.data.mongodb.core.MongoTemplate: DEBUG&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;readConern과 writeConcern은 큰 문제가 없다면 기본값인 &lt;b&gt;local&lt;/b&gt;과 &lt;b&gt;w : 1, j : true&lt;/b&gt; 사용이 권장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.mongodb.com/community/forums/t/why-can-t-read-preference-be-secondary-in-a-transaction/204432&quot;&gt;https://www.mongodb.com/community/forums/t/why-can-t-read-preference-be-secondary-in-a-transaction/204432&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;replicaset의 활용성을 높이기 위해 readPreference을 secondaryPreferred로 지정해 줄 수 있지만 만약 프로젝트에서 Trasnactional을 사용하는 경우 해당 옵션은 기본값은 primary를 사용해야만 합니다. MongoDB의 경우 트랜잭션은 기본적으로 모든 읽기, 쓰기 작업은 일관성을 유지하기 위해 primary노드에서 수행되어야 하기 때문입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했듯이 WiredTiger는 트랜잭션을 사용 시, isoliation이 snapshot으로 고정됩니다. 따라서 @Transactional(readOnly = true)를 사용하면 트랜잭션 시작 시점에 생성한 snapshot을 기반으로 일관된 읽기가 가능해집니다. 반면에 @Transactional(readOnly = true)을 사용하지 않으면 readConcern에 따른 읽기 일관성이 제공됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MongoClientConfig&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;/**
 * https://www.baeldung.com/spring-data-mongodb-transactions
 * https://docs.spring.io/spring-data/mongodb/docs/current-SNAPSHOT/reference/html/#mongo.transactions.tx-manager
 */
@Configuration
@EnableMongoAuditing
@EnableMongoRepositories(basePackages = [&quot;com.example.mongo.repository&quot;])
class MongoClientConfig(
    @Value(&quot;\${spring.data.mongodb.database}&quot;) private val database: String,
    @Value(&quot;\${spring.data.mongodb.uri}&quot;) private val uri: String,
) : AbstractMongoClientConfiguration() {

    @Bean(&quot;mongoTransactionManager&quot;)
    fun mongoTransactionManager(dbFactory: MongoDatabaseFactory): MongoTransactionManager {
        return MongoTransactionManager(dbFactory)
    }

    override fun getDatabaseName(): String {
        return database
    }

    override fun mongoClient(): MongoClient {
        return MongoClients.create(
            MongoClientSettings.builder()
                .applyConnectionString(ConnectionString(uri))
                .build(),
        )
    }

    override fun mappingMongoConverter(
        databaseFactory: MongoDatabaseFactory,
        customConversions: MongoCustomConversions,
        mappingContext: MongoMappingContext,
    ): MappingMongoConverter {
        super.mappingMongoConverter(databaseFactory, customConversions, mappingContext)
        val dbRefResolver: DbRefResolver = DefaultDbRefResolver(databaseFactory)
        val mappingConverter = MappingMongoConverter(dbRefResolver, mappingContext)

        mappingConverter.customConversions = customConversions

        // db에 _class 컬럼을 남기지 않는 설정
        mappingConverter.setTypeMapper(DefaultMongoTypeMapper(null))

        return mappingConverter
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 spring boot는 txManager를 자동으로 빈으로 등록해 주지만 mongoDB는 tx사용이 선택적이라 자동으로 빈을 등록해주지 않습니다. 따라서 mongoDB tx 사용을 위해서는 별도의 빈을 등록해야 합니다. mongoTypeMapper를 설정해 주는 이유는 아무런 설정이 없다면 db에 저장될 때 _class값이 메타데이터로 저장되는데 이 값을 없애기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;document&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Document(DOCUMENT_NAME)
class PipelineDocument(
    @Field(ID) @Id val id: ObjectId? = null,
    @Field(STEPS) val steps: List&amp;lt;StepDocument&amp;gt;,
    @Field(STATUS) val status: String,
    @Field(REGISTERED_BY) val registeredBy: String,
    @Field(REGISTERED_DATE) val registeredDate: LocalDateTime,
) {

    @Document(StepDocument.DOCUMENT_NAME)
    data class StepDocument(
        @Field(TYPE) val type: String,
        @Field(STATUS) val status: String,
        @Field(REGISTERED_BY) val registeredBy: String,
        @Field(REGISTERED_DATE) val registeredDate: LocalDateTime,
    ) {

        companion object {
            const val DOCUMENT_NAME = &quot;step&quot;
            const val TYPE = &quot;type&quot;
            const val STATUS = &quot;status&quot;
            const val REGISTERED_BY = &quot;registeredBy&quot;
            const val REGISTERED_DATE = &quot;registeredDate&quot;
        }
    }

    companion object {
        const val DOCUMENT_NAME = &quot;pipeline&quot;
        const val ID = &quot;_id&quot;
        const val STEPS = &quot;steps&quot;
        const val STATUS = &quot;status&quot;
        const val REGISTERED_BY = &quot;registeredBy&quot;
        const val REGISTERED_DATE = &quot;registeredDate&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 document를 기반으로 진행해 보겠습니다. @Document는 Jpa의 @Entity와 유사하며 @Id는 pk를 의미합니다. mongoDB에서는 ObjectId 타입을 사용하고 null값인 경우 db에서 auto-generate를 해줍니다.&lt;/p&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;repository&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface PipelineDocumentRepository : MongoRepository&amp;lt;PipelineDocument, ObjectId&amp;gt;, PipelineDocumentCustomRepository {
    fun findByStepsTypeIn(stepsTypes: List&amp;lt;String&amp;gt;): List&amp;lt;PipelineDocument&amp;gt;
}

interface PipelineDocumentCustomRepository {
    fun findByStepStatusIn(stepsStatuses: List&amp;lt;String&amp;gt;): List&amp;lt;PipelineDocument&amp;gt;
    fun countByStepStatusIn(stepsStatuses: List&amp;lt;String&amp;gt;): Long
    fun findPageByStepStatusIn(stepsStatuses: List&amp;lt;String&amp;gt;, pageable: Pageable): List&amp;lt;PipelineDocument&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data Mongodb는 jpa와 유사한 MongoRepository 인터페이스를 제공하여 다양한 CRUD 메서드를 자동으로 만들어줍니다. 이외의 복잡한 쿼리는 QueryDSL과 유사하게 추가 인터페이스를 사용하여 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class PipelineDocumentCustomRepositoryImpl(
  private val mongoTemplate: MongoTemplate,
): PipelineDocumentCustomRepository {

  override fun findByStepStatusIn(stepsStatuses: List&amp;lt;String&amp;gt;): List&amp;lt;PipelineDocument&amp;gt; {
    return mongoTemplate.find(
      PipelineDocumentQueryBuilder.buildQueryToFindByStepStatusIn(stepsStatuses),
      PipelineDocument::class.java
    )
  }

  override fun countByStepStatusIn(stepsStatuses: List&amp;lt;String&amp;gt;): Long {
    return mongoTemplate.count(
      PipelineDocumentQueryBuilder.buildQueryToFindByStepStatusIn(stepsStatuses),
      PipelineDocument::class.java
    )
  }

  override fun findPageByStepStatusIn(stepsStatuses: List&amp;lt;String&amp;gt;, pageable: Pageable): List&amp;lt;PipelineDocument&amp;gt; {
    return mongoTemplate.find(
      PipelineDocumentQueryBuilder.buildQueryToFindPageByStepStatusIn(stepsStatuses, pageable),
      PipelineDocument::class.java
    )
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 인터페이스 구현은 mongoTemplate을 활용하여 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;object PipelineDocumentQueryBuilder {

    fun buildQueryToFindByStepStatusIn(stepStatuses: List&amp;lt;String&amp;gt;): Query {
        return Query().addCriteria(
            Criteria.where(&quot;${PipelineDocument.STEPS}.${PipelineDocument.STATUS}&quot;).`in`(stepStatuses)
        )
    }

    fun buildQueryToFindPageByStepStatusIn(stepStatuses: List&amp;lt;String&amp;gt;, pageable: Pageable): Query {
        return Query().addCriteria(
            Criteria.where(&quot;${PipelineDocument.STEPS}.${PipelineDocument.STATUS}&quot;).`in`(stepStatuses)
        )
            .with(pageable)
            .with(Sort.by(Sort.Direction.DESC, &quot;$PipelineDocument.ID&quot;))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용되는 쿼리는 재사용성을 위해 추가의 object 클래스로 분리하여 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Transaction Write Conflict&lt;/h2&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;788&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYvHfg/btsIiiKrfJk/YRMJVKsRKjIrAQah7u0QHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYvHfg/btsIiiKrfJk/YRMJVKsRKjIrAQah7u0QHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYvHfg/btsIiiKrfJk/YRMJVKsRKjIrAQah7u0QHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYvHfg%2FbtsIiiKrfJk%2FYRMJVKsRKjIrAQah7u0QHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;596&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;1174&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;트랜잭션 1 시작&lt;/li&gt;
&lt;li&gt;트랜잭션 2 시작&lt;/li&gt;
&lt;li&gt;트랜잭션 1에서 document 1 수정 후 업데이트&lt;/li&gt;
&lt;li&gt;트랜잭션 2에서 document 1 수정 후 업데이트 시도 -&amp;gt; 실패&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상황에서 트랜잭션 1은 정상적으로 수행되지만, 트랜잭션 2는 Write Conflict Error를 발생시키며 롤백됩니다. 도큐먼트의 버전을 확인하여 이미 수정되지 않은 경우에만 수정하는 낙관적 동시성 제어 방식을 사용하는 것입니다. 또한 트랜잭션 1이 커밋되기 전에 트랜잭션 3이 시작돼버리면 트랜잭션 3도 Write Conflict Error가 발생하므로 재시도는 트랜잭션 1이 커밋되고 난 후에 진행되어야 합니다. 이는 &lt;a href=&quot;https://www.mongodb.com/docs/manual/core/transactions-production-consideration/#in-progress-transactions-and-write-conflicts&quot;&gt;공식 문서&lt;/a&gt;에도 설명되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/mongodb/docs/current-SNAPSHOT/reference/html/#introduction&quot;&gt;Spring Mongo 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-data-mongodb-transactions&quot;&gt;Spring Data MongoDB Transactions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wonyong-jang.github.io/aws/2021/05/24/AWS-DocumentDB-Read-Write-Option.html&quot;&gt;DocumentDB Read Preference, Write Concern 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spongelog.netlify.app/mongodb-multi-document-transaction-write-conflict/&quot;&gt;MongoDB Multi-document transaction Write Conflict Internals&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <category>MongoDB</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/90</guid>
      <comments>https://backtony.tistory.com/90#entry90comment</comments>
      <pubDate>Sun, 30 Jun 2024 16:31:15 +0900</pubDate>
    </item>
    <item>
      <title>Reactive 환경에서 Redisson 분산락 적용하기</title>
      <link>https://backtony.tistory.com/89</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는 &lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/database/distributed-lock/distributed-lock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;가 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분산 서버 동시성 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1582&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c17jYm/btsIg82ZpiY/GHCwQNgY5cmeM0lpw8Fbsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c17jYm/btsIg82ZpiY/GHCwQNgY5cmeM0lpw8Fbsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c17jYm/btsIg82ZpiY/GHCwQNgY5cmeM0lpw8Fbsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc17jYm%2FbtsIg82ZpiY%2FGHCwQNgY5cmeM0lpw8Fbsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;848&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;848&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;ApiGateway로 인해 여러 서버로 분산된 요청들이 DB의 같은 자원에 접근해서 수정하게 되는 경우, 최종적으로 커밋된 데이터의 형태로 자원이 저장되게 됩니다. 예를 들어, A 요청이 먼저 들어와서 1이라는 자원을 수정하고 있는데, B요청이 들어와서 A보다 먼저 자원을 수정하고 커밋하고 이후에 A가 커밋하게 된다면 최종적으로는 A에서 커밋한 데이터가 반영되게 되면서 정합성이 깨지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분삭락을 사용하게 되는 경우, 가장 먼저 들어온 요청이 락을 획득하여 작업을 처리하기 때문에 이후 들어온 요청은 락을 획득하기까지 기다리고 이전 작업이 락을 해제해야만 다음 요청이 락을 획득하여 작업을 처리할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redisson 라이브러리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 사용하게 되면 보통 Lettuce 구현체를 사용하게 됩니다. Lecttuce를 사용하여 분산락을 구현할 경우, setnx,setex과 같은 명령어를 통해 지속적으로 Redis에게 락이 해제되었는지 요청을 보내 확인하는 스핀락 방식으로 동작합니다. 이 경우 요청이 많아질수록 Redis가 받는 부하가 커지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 비해 Redisson은 Pub/Sub 방식을 이용하기 때문에 락이 해제되면 락을 subscribe하는 클라이언트가 락이 해제되었다는 신호를 받고 락 획득을 시도하게 됩니다. 뿐만 아니라 Lock interface를 별도로 지원하여 락에 대한 타임아웃과 같은 설정을 통해 더욱 안전하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle.kts&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)
implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis-reactive&quot;)
implementation(&quot;org.apache.commons:commons-pool2:2.12.0&quot;)

implementation(&quot;org.redisson:redisson-spring-boot-starter:3.31.0&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-redis 라이브러리는 redis의 커넥션 풀을 지원하지 않기 때문에 사용하고자 한다면 &lt;a href=&quot;https://docs.spring.io/spring-data/redis/docs/3.1.6/reference/html/#redis:connectors:connection&quot;&gt;공식문서&lt;/a&gt;에서는 commons-pool을 의존성으로 추가하길 가이드하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  data:
    redis:
      url: localhost
      port: 6379
      connect-timeout: 3000
      timeout: 3000&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RedisConfig&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 추가로 인해 기본적인 세팅은 RedisAutoConfiguration과 RedissonAutoConfiguration으로 인해 자동으로 세팅됩니다. 따라서 필요한 pool설정을 위해 redisConnectionFactory와 RedissonClient만 빈으로 등록해 주면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class RedisConfig(
    private val redisProperties: RedisProperties,
) {

    @Bean
    fun redissonClient(): RedissonClient {
        val config = Config()
        val poolConfig = lettucePoolingClientConfiguration().poolConfig
        config.useSingleServer().setAddress(&quot;redis://${redisProperties.url}:${redisProperties.port}&quot;)
            .setConnectionPoolSize(poolConfig.maxTotal)
            .setConnectionMinimumIdleSize(poolConfig.minIdle)
            .setTimeout(redisProperties.timeout.toMillis().toInt())
            .setConnectTimeout(redisProperties.connectTimeout.toMillis().toInt())
            .setIdleConnectionTimeout(3_000)
        return Redisson.create(config)
    }

    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        return LettuceConnectionFactory(
            RedisStandaloneConfiguration(redisProperties.url, redisProperties.port),
            lettucePoolingClientConfiguration(),
        )
        // 아래는 클러스터 설정
//        return LettuceConnectionFactory(RedisClusterConfiguration(listOf(redisProperties.url)), lettuceClientConfiguration)
    }

    private fun lettucePoolingClientConfiguration(): LettucePoolingClientConfiguration {
        val topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enableAllAdaptiveRefreshTriggers()
            .enablePeriodicRefresh(Duration.ofSeconds(30)) // 기본값이 disabled이므로 설정 필수, 권장 주기 30초
            .dynamicRefreshSources(true) // 기본값이 true이므로 설정하지 않아도 되지만 false로 변경은 금지
            .build()


        val timeoutOptions = TimeoutOptions.builder()
            .timeoutCommands()
            .fixedTimeout(redisProperties.timeout) // 사용 용도에 따라 자유롭게 설정
            .build()

        val clientOptions = ClusterClientOptions.builder()
            .topologyRefreshOptions(topologyRefreshOptions)
            .timeoutOptions(timeoutOptions)
            .build()

        return LettucePoolingClientConfiguration.builder()
            .clientOptions(clientOptions)
            .poolConfig(GenericObjectPoolConfig&amp;lt;Any&amp;gt;())
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분산락 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Redisson의 락은 특정 스레드에 바인딩되어 있어서 락을 건 스레드만이 해당 락을 해제할 수 있도록 설계되어 있습니다. 락을 건 스레드와 락을 해제하는 스레드가 다르면 데드락(교착 상태)이나 레이스 컨디션과 같은 동기화 문제가 발생할 수 있기 때문에 동기화 문제와 데이터의 일관성을 보장하기 위해 이렇게 구현되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코루틴의 경우 실제 스레드와 독립적으로 실행되기 때문에 락을 건 스레드와 해제하는 스레드가 다를 수 있어 락을 건 스레드와 해당 락을 해제하려는 스레드가 다르다는 아래와 같은 예외가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3832&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZUreZ/btsIicXD9bl/Tebr3NYnqidUe4pcAmHly0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZUreZ/btsIicXD9bl/Tebr3NYnqidUe4pcAmHly0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZUreZ/btsIicXD9bl/Tebr3NYnqidUe4pcAmHly0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZUreZ%2FbtsIicXD9bl%2FTebr3NYnqidUe4pcAmHly0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3832&quot; height=&quot;254&quot; data-origin-width=&quot;3832&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Redisson에서는 이를 위한 해결책으로 threadId를 인자로 받는 API를 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;/**
 * Returns Lock instance by name.
 * &amp;lt;p&amp;gt;
 * Implements a &amp;lt;b&amp;gt;non-fair&amp;lt;/b&amp;gt; locking so doesn't guarantees an acquire order by threads.
 * &amp;lt;p&amp;gt;
 * To increase reliability during failover, all operations wait for propagation to all Redis slaves.
 *
 * @param name name of object
 * @return Lock object
 */
RLockReactive getLock(String name);

/**
 * Tries to acquire the lock by thread with specified &amp;lt;code&amp;gt;threadId&amp;lt;/code&amp;gt; and  &amp;lt;code&amp;gt;leaseTime&amp;lt;/code&amp;gt;.
 * Waits up to defined &amp;lt;code&amp;gt;waitTime&amp;lt;/code&amp;gt; if necessary until the lock became available.
 *
 * Lock will be released automatically after defined &amp;lt;code&amp;gt;leaseTime&amp;lt;/code&amp;gt; interval.
 *
 * @param threadId id of thread
 * @param waitTime time interval to acquire lock
 * @param leaseTime time interval after which lock will be released automatically 
 * @param unit the time unit of the {@code waitTime} and {@code leaseTime} arguments
 * @return &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; if lock acquired otherwise &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;
 */
Mono&amp;lt;Boolean&amp;gt; tryLock(long waitTime, long leaseTime, TimeUnit unit, long threadId);

/**
 * Unlocks the lock. Throws {@link IllegalMonitorStateException}
 * if lock isn't locked by thread with specified &amp;lt;code&amp;gt;threadId&amp;lt;/code&amp;gt;.
 *
 * @param threadId id of thread
 * @return void
 */
Mono&amp;lt;Void&amp;gt; unlock(long threadId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getLock: 지정한 name으로 lock 인스턴스를 획득합니다.&lt;/li&gt;
&lt;li&gt;tryLock : lock을 획득합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;waitTime : lock 획득에 기다리는 시간&lt;/li&gt;
&lt;li&gt;leaseTime : lock이 leaseTime내에 해제되지 않으면 자동으로 해제됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;unlock : lock을 해제합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coroutine에서는 마땅히 넣어줄 threadId가 존재하지 않기 때문에 이를 대신해줄 값으로 SecureRandom을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class DistributedLockUtils(
    private val redissonReactiveClient: RedissonReactiveClient,
    private val transactionUtils: TransactionUtils,
) {

    val random = SecureRandom()

    suspend fun &amp;lt;T&amp;gt; run(targetClassName: String, id: String, block: suspend () -&amp;gt; T): T {
        val uniqueId = random.nextLong()
        val lockName = &quot;$targetClassName:$id&quot;
        val lock = redissonReactiveClient.getLock(lockName)

        val available = lock.tryLock(TRY_LOCK_TIME_OUT, LEASE_TIME, TimeUnit.SECONDS, uniqueId).awaitSingle()

        check(available) { &quot;Fail to get lock $lockName. Acquire lock timeout.&quot; }

        try {
            return transactionUtils.executeInNewTransaction(
                timeoutSecond = TARGET_METHOD_TIME_OUT,
                operation = { block.invoke() }
            )
        } catch (ex: Exception) {
            when (ex) {
                is TimeoutCancellationException -&amp;gt; {
                    throw IllegalStateException(
                        &quot;Target Method timeout Lock lease will be release. LockName : $lockName &quot;, ex
                    )
                }

                else -&amp;gt; throw ex
            }
        } finally {
            withContext(NonCancellable) {
                lock.unlock(uniqueId).awaitSingleOrNull()
            }
        }
    }

    companion object {
        // 획득까지 대기 시간
        private const val TRY_LOCK_TIME_OUT = 5L

        // 획득 이후 잡고 있을 시간, 이 시간이 지나도 unlock되지 않으면 자동으로 unlock
        private const val LEASE_TIME = 4L

        private const val TARGET_METHOD_TIME_OUT = 3L
    }
}&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;trailing lambda 방식으로 구현한 DistributedLockUtils 클래스입니다. threadId 대신 secureRandom의 nextLong 값을 사용해서 채웠습니다. leaseTime이 지나면 lock이 자동으로 해제되기 때문에 다른 요청이 락을 획득해서 사용하게 되면 문제가 발생할 수 있습니다. 따라서 아래 transactionaUtils 클래스에서 LeaseTime보다 작은 값으로 target 메서드를 호출하는 로직에 withTimeout으로 감싸서 제한시간 내에 메서드 수행이 완료되지 않는 경우 예외를 발생시키도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock을 획득하는 코드와 실제 타겟 메서드 호출의 트랜잭션을 분리해하기 때문에 해당 메서드의 호출은 transactioUtils 클래스에서 처리합니다. 만약 같은 트랜잭션에서 호출하게 되는 경우, 타겟 메서드의 트랜잭션이 커밋되지 않은 상태에서 락을 해제하게 되므로 데이터 정합성을 보장할 수 없게 되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class TransactionUtils {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    suspend fun &amp;lt;T&amp;gt; executeInNewTransaction(
        timeoutSecond: Long = -1,
        operation: suspend () -&amp;gt; T,
    ): T {
        if (timeoutSecond == -1L) {
            return operation()
        }

        try {
            return withTimeout(timeoutSecond.toDuration(DurationUnit.SECONDS)) {
                operation()
            }
        } catch (ex: Exception) {
            throw ex
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 coroutine에 대한 지원이 없어 약간의 우회를 통해 사용했지만 &lt;a href=&quot;https://github.com/redisson/redisson/issues/5667&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이슈&lt;/a&gt;에 feature로 등록된 것으로 보아 추후 기능이 추가될 것으로 예상됩니다. aop 대신 trailing lambda 문법을 사용한 이유는 아직 spring coroutine 환경에서 aop와 transactional을 동시에 사용하면 버그가 있기 때문입니다. 관련 내용은 &lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/33095&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 이슈&lt;/a&gt;에서 해결중입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/&quot;&gt;풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/java-uuid-unique-long-generation&quot;&gt;Generating Unique Positive long Using UUID in Java&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://betterprogramming.pub/dont-use-java-redissonclient-with-kotlin-coroutines-for-distributed-redis-locks-41da2e85c54a&quot;&gt;Don&amp;rsquo;t Use Java RedissonClient With Kotlin Coroutines for Distributed Redis Locks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <category>lock</category>
      <category>REDIS</category>
      <category>Redisson</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/89</guid>
      <comments>https://backtony.tistory.com/89#entry89comment</comments>
      <pubDate>Sat, 29 Jun 2024 18:18:22 +0900</pubDate>
    </item>
    <item>
      <title>Spring - GraphQL 서버 구축하기</title>
      <link>https://backtony.tistory.com/88</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는 &lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/armeria%20grpc%20graphql/armeria&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GraphQL 이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Graph QL(이하 gql)은 Structed Query Language(이하 sql)와 마찬가지로 쿼리 언어입니다. 하지만 gql과 sql의 언어적 구조 차이는 매우 큽니다. 또한 gql과 sql이 실전에서 쓰이는 방식의 차이도 매우 큽니다. gql과 sql의 언어적 구조 차이가 활용 측면에서의 차이를 가져왔습니다. 이 둘은 애초에 탄생 시기도 다르고 배경도 다릅니다. sql은 데이터베이스 시스템에 저장된 데이터를 효율적으로 가져오는 것이 목적이고, gql은 웹 클라이언트가 데이터를 서버로부터 효율적으로 가져오는 것이 목적입니다. sql의 문장(statement)은 주로 백앤드 시스템에서 작성하고 호출하는 반면, gql의 문장은 주로 클라이언트 시스템에서 작성하고 호출합니다.&lt;/p&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;SQL 쿼리 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM member;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;gql 쿼리 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;query team($id: Long!) {
     team(id: $id) {
        id
        name
        registeredBy
        registeredDate
        modifiedBy
        modifiedDate
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://graphql.org/learn/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;graphQL 공식 문서&lt;/a&gt;에서는 graphQL을 API용 쿼리 언어이자, 데이터에 대해 정의한 타입 시스템을 사용하여 쿼리를 실행하기 위한 server runtime 이라고도 소개합니다. 서비스 측면에서 graphQL 서비스는 API가 노출하는 데이터 구조를 설명하는 runtime layer를 제공하는데 runtime layer는 graphQL 요청을 파싱하고 각 필드에 대해 적절한 데이터 페처(resolver)를 호출하는 역할을 담당합니다. 여기서 개발자는 각 필드에 대한 데이터 페처(resolver)를 구현하게 됩니다.&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;1490&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/snQja/btsHtOdiWw0/KraVkcuP2Pksc2CLImjIVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/snQja/btsHtOdiWw0/KraVkcuP2Pksc2CLImjIVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/snQja/btsHtOdiWw0/KraVkcuP2Pksc2CLImjIVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsnQja%2FbtsHtOdiWw0%2FKraVkcuP2Pksc2CLImjIVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1490&quot; height=&quot;1126&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;1126&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;서버사이드 gql 애플리케이션은 gql로 작성된 쿼리를 입력으로 받아 쿼리를 처리한 결과를 다시 클라이언트로 돌려줍니다. HTTP API 자체가 특정 데이터베이스나 플랫폼에 종속적이지 않은 것처럼 gql 역시 어떠한 특정 데이터베이스나 플렛폼에 종속적이지 않습니다. 심지어 네트워크 방식에도 종속적이지 않습니다. 일반적으로 gql의 인터페이스 간 송수신은 네트워크 레이어 L7의 HTTP POST 메서드와 웹소켓 프로토콜을 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GraphQL vs Rest API&lt;/h3&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;1492&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpmb5u/btsHuOJ0wTz/wkx85TfL3Jmvsd120x69U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpmb5u/btsHuOJ0wTz/wkx85TfL3Jmvsd120x69U0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpmb5u/btsHuOJ0wTz/wkx85TfL3Jmvsd120x69U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpmb5u%2FbtsHuOJ0wTz%2Fwkx85TfL3Jmvsd120x69U0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1492&quot; height=&quot;826&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;826&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;REST API는 URL, METHOD 등을 조합하기 때문에 다양한 Endpoint가 존재합니다. 반면, gql은 단 하나의 Endpoint가 존재 합니다. 또한, gql API에서는 불러오는 데이터의 종류를 쿼리 조합을 통해 결정합니다. 예를 들면, REST API에서는 각 Endpoint마다 데이터베이스 SQL 쿼리가 달라지는 반면, gql API는 gql 스키마의 타입마다 데이터베이스 SQL 쿼리가 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rest API는 HTTP 요청방식 (GET, POST, PUT DELETE 등)을 사용하여 데이터를 요청하고 응답받습니다. 따라서, 상황에 따라 다른 Method를 사용해야 하고 api별로 각각의 다른 end point를 갖습니다. 반면, GraphQL은 동일하게 HTTP 요청방식을 사용하지만 POST Method와 단일 end point만 사용하며 응답받는 데이터는 쿼리 조합을 통해 결정됩니다. 위 그림에서 Rest API의 경우 post, comment, author 데이터를 조회하기 위해서는 3번의 호출이 필요했지만, graphql에서는 하나의 쿼리 요청에 3가지 데이터를 포함할 수 있기 때문에 한 번의 네트워크 호출로 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rest API를 사용하면 같은 url을 사용할 경우, 항상 동일한 데이터 구조의 응답 결과를 받게 됩니다. 경우에 따라 응답 데이터의 모든 필드가 필요하지 않을 수 있는데 모든 필드를 응답받게 되는 over fetching이 발생하는 경우가 있습니다. 하지만 GraphQL의 경우 응답 데이터의 필요한 필드만 선택해서 받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Schema&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL 스키마는 namespace를 지원하지 않습니다. 따라서 모든 Object의 이름이 중복되면 안 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Object&lt;/h4&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;type Book {
    id : Int
    name : String!
    author: String!
    price: Float
    ratings: [Rating]
}

type Rating {
    id: Int
}&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;GraphQL의 스키마는 object 타입으로 구성되며, 기본적으로 아래와 같은 scalar 타입을 제공합니다.&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;INT&lt;/li&gt;
&lt;li&gt;Float&lt;/li&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;li&gt;Boolean&lt;/li&gt;
&lt;li&gt;ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;! 는&lt;/code&gt; non-null을 의미하며, 대괄호([])는 리스트를 의미합니다.&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;Enum&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바와 유사한 ENUM 타입도 제공됩니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum Episode {
    NEWHOPE
    EMPIRE
    JEDI
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;interface&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바와 유사한 Interface 타입도 제공됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;interface Character {
    id: ID!
    name: String!
}

type Human implements Character {
    id: ID!
    name: String!
    age: Int!
}

type Droid implements Character {
    id: ID!
    name: String!
    expiredAt: String!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Mutation과 Query&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rest API에서는 HTTP method(GET, POST..)를 사용했던 것처럼 GraphQL에는 3가지 Operation이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;query : 조회(read)&lt;/li&gt;
&lt;li&gt;mutation : 쓰기(write)&lt;/li&gt;
&lt;li&gt;subscription : 지속적인 읽기(websocket)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;type Query {
    hero(episode: Episode!): Character
}

type Mutation {
    addBook(name: String!, author: String!, publisher: String!, price: Float!): BookInfo!
}&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;Query와 Mutation의 정의는 최종적으로 client가 호출하게 되는 API 명세라고 봐도 무방합니다.&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;Input&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스칼라 인자로 API를 정의하는 대신, input type으로 복잡한 객체를 정의할 수 있습니다. 바로 위의 Mutation의 경우 스칼라 인자가 4개 이므로 input type으로 처리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;type Mutation {
    addBook(input: BookInput!): BookInfo!
}

input BookInput {
    name : String
    author: String
    publisher: String
    price: Float
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fragment&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment는 쿼리에서 반복적으로 사용되는 필드 목록을 묶는 단위로 복잡성을 낮추고 재사용성을 위해 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;query {
    lauches() {
        lauch_year
        rocket {
            rocket_name
            rocket {
                ...RocketDetail
            }
        }
    }
    rockets {
        ...RocketDetail
    }    
}

fragment RocketDetail on Rocket {
    name
    company
    boosters
    height {
        feet
        meters
    }
}&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;Rocket 정보 중 필요한 정보 모음을 RocketDetail이라는 이름의 프레그먼트로 정의했습니다. 쿼리 중 Rocket 타입을 반환받는 모든 곳에서 ...RocketDetail과 같이 사용할 수 있습니다. 반복적으로 입력하지 않아도 되므로 쿼리 작성 효율성과 가독성 향상에 도움이 됩니다. 또한 GraphQL 클라이언트 라이브러리에서는 Fragment를 기준으로 캐시를 구성하기도 하므로 중요하게 사용됩니다.&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;Union&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입의 집합을 정의할 때 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;union SearchResult = Human | Driod | Starship

type Query {
    search(text: String!): SearchResult
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Directive&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;directive는 GraphQL 스키마의 일부를 애노테이션 처리하여 추가적인 동작을 처리하는 방법입니다. @ 문자로 시작하는 지시자를 정의할 수 있으며, 이름, 인자(선택 사항) 및 실행 위치가 같이 정의됩니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 내장되어 제공되는 deprecated directive
directive @deprecated(
  reason: String = &quot;No longer supported&quot;
) on FIELD_DEFINITION | ENUM_VALUE&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;디렉티브의 이름은 @deprecated이고 reason 필드를 가지고 있으며 default 값이 명시되어 있습니다. 이 directive는 필드 정의와 enum 값에 적용할 수 있어 아래와 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;type Book {
    id: ID
    bookName: String
    name: String @deprecated(reason: &quot;Use `bookName`.&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;query findFilm($filmId: ID, $withProducer: Boolean = false) {
    film(id: $filmId) {
        director
        title
        releaseDate
        producers @include(if:$withProducers)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 &lt;a href=&quot;https://graphql.org/learn/queries/#directives&quot;&gt;공식 문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring GraphQL&lt;/h2&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;graphql-java / graphql-java-spring&lt;/li&gt;
&lt;li&gt;graphql-java-kickstart / graphql-spring-boot&lt;/li&gt;
&lt;li&gt;Netflix / dgs-framework&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Java 진영에서 Spring과 함께 GraphQL을 사용하기 위해서는 위 3개가 대표적인 라이브러리/프레임워크로 3개 중 1개를 선택해 스프링에서 graphql을 사용했었습니다. 이 중 graphql-java에서 spring에서 사용하기 위해 제공하던 graphql-java-spring 라이브러리가 spring project로 이전되어 spring-graphql로 변경되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 라이브러리/프레임워크를 사용하면 Resolver를 개발하고 별도의 설정이 필요했지만, Spring for GraphQL은 graphql-java의 단순 후속 프로젝트뿐 아니라 graphql-java 개발팀이 개발해서 Spring이 추구하는 방향답게 추가적인 코드 없이 기존 MVC 개발하는 방식대로 개발이 가능해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle.kst&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-validation&quot;)

    // graphql
    implementation(&quot;org.springframework.boot:spring-boot-starter-graphql&quot;)
    implementation(&quot;org.springframework.data:spring-data-commons&quot;)
    implementation(&quot;com.graphql-java:graphql-java-extended-scalars:22.0&quot;)
    implementation(&quot;com.tailrocks.graphql:graphql-datetime-spring-boot-starter:6.0.0&quot;)
    implementation(&quot;com.github.ben-manes.caffeine:caffeine&quot;)

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;com.graphql-java:graphql-java-extended-scalars : 기본적으로 GraphQL에서 제공하는 scalar 외의 Java의 scalar를 제공합니다.&lt;/li&gt;
&lt;li&gt;com.tailrocks.graphql:graphql-datetime-spring-boot-starter : java의 LocalDate 관련 scalar Scalar를 제공합니다.&lt;/li&gt;
&lt;li&gt;com.github.ben-manes.caffeine:caffeine : schema 로컬 캐시를 위해 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;runtimeWiring&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring GraphQL은 mvc처럼 애노테이션 방식으로 구성할 수 있습니다. 하지만 애노테이션 방식으로 바로 구현하기 전에 애노테이션 방식을 사용하지 않고 GraphQL Java API를 사용하여 저수준 방식으로 구현하면서 중요한 개념을 몇 가지 다뤄보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;type Query {
    member(id: Long!): Member
}

type Member {
    id: Long!
    name: String!
    # 생략    
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;  @Bean
fun runtimeWiringConfigurer(memberService: MemberService): RuntimeWiringConfigurer {
    return RuntimeWiringConfigurer { builder -&amp;gt;
        builder.type(&quot;Query&quot;) { typeBuilder -&amp;gt;
            typeBuilder
                .dataFetcher(&quot;member&quot;, DataFetcher { environment: DataFetchingEnvironment -&amp;gt;
                    memberService.getMember(environment.getArgument&amp;lt;Long&amp;gt;(&quot;id&quot;))
                })
        }
            .scalar(ExtendedScalars.GraphQLLong)
            .scalar(ExtendedScalars.Json)
            .scalar(LocalDateTimeScalar.create(null, true, null))
            .scalar(LocalDateScalar.create(null, true, null))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL Java의 RuntimeWiring.Builder는 DataFetchers, TypeRevolser, Custom Scalar Type 등을 등록하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실질적인 로직은 RuntimeWiringConfigurer에 dataFetcher를 등록함으로써 graphQL API 호출에 대해 로직이 동작합니다. 위 코드는 member 필드 데이터를 가져오는 dataFetcher를 등록한 코드입니다. DataFetcher는 GraphQL Java 서버에서 가장 중요한 개념 중 하나입니다. GraphQL API 요청을 받으면 쿼리가 실행되는 동안 쿼리에서 요청한 각 필드들에 대해 적절한 DataFetcher가 호출됩니다. 스키마의 모든 필드에는 연결된 DataFetcher가 존재하는데 특정 필드에 대해 DataFetcher를 지정하지 않으면 기본값으로 PropertyDataFetcher가 사용됩니다. 위의 예시에서는 member 필드에 대한 dataFetcher는 등록했지만 member.id, member.name의 필드에 대한 dataFetcher는 등록하지 않았습니다. 따라서 PropertyDataFetcher가 기본값으로 등록되어 getXX를 통해 해당 필드 데이터를 가져오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL은 Int, Float, Boolean, ID, String 스칼라 타입만 제공합니다. 별도의 커스텀 스칼라를 추가하기 위해 build.gradle 의존성에 scalar 관련 라이브러리를 추가해 줬었습니다. 이를 활용하여 scalar를 추가할 수 있습니다. 위와 같이 scalar를 추가한 경우 graphql schema에서 아래와 같이 명시한 scalar를 읽어올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;scalar LocalDate
scalar LocalDateTime
scalar Long

type Member {
    id: Long! # custom scalar
    registeredDate: LocalDateTime! # custom scalar
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 애노테이션 방식을 알아보겠습니다. 앞서 언급했듯이, spring GraphQL은 애노테이션 방식으로 MVC 방식과 유사하게 개발할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;type Mutation {
    createMember(input: CreateMemberInput!): Member!
}

type Query {
    member(id: Long!): Member
}

type Member {
    id: Long!
    name: String!
    teamId: Long
    team: Team
}

type Team {
    id: Long!
    name: String!
}

input CreateMemberInput {
    name: String! 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 스키마를 기준으로 아래와 같은 Controller를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
class MemberController(
    private val memberService: MemberService,
    private val teamService: TeamService,
)  {

    private val log = KotlinLogging.logger { }

    @QueryMapping
    suspend fun member(@Argument id: Long, env: DataFetchingEnvironment): Member? {
        return memberService.getMember(id)
    }

    @MutationMapping
    suspend fun createMember(@Argument input: CreateMemberInput, env: DataFetchingEnvironment): Member {
        return memberService.createMember(input)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 @Controller 빈을 감지하고 아래 애노테이션이 붙은 핸들러 메서드를 DataFetcher로 등록합니다.&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;SchemaMapping : query, mutation 모두 처리합니다.&lt;/li&gt;
&lt;li&gt;QueryMapping : query operation을 처리합니다.&lt;/li&gt;
&lt;li&gt;MutationMapping : mutation operation을 처리합니다.&lt;/li&gt;
&lt;li&gt;BatchMapping : N+1 문제를 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BatchMapping을 제외한 위 세 가지는 MVC의 RequestMapping, PostMapping, GetMapping과 유사합니다. PostMapping과 GetMapping이 RequestMapping의 축약형이듯이, MutationMapping과 QueryMapping은 SchemaMapping의 축약형입니다. 실제로 애노테이션을 까보면 아래와 같이 포함되어 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 @Argument는 인자를 받는 데 사용됩니다. MVC 관점에서 @RequestBody와 유사하다고 인지하면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@SchemaMapping(typeName = &quot;Query&quot;)
public @interface QueryMapping

@SchemaMapping(typeName = &quot;Mutation&quot;)
public @interface MutationMapping&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;@Controller 애노테이션이 붙은 클래스를 만들고, Schema 명세에 맞는 메서드에 @MutationMapping, @QueryMapping 애노테이션을 마킹해 주면 실질적으로 graphql의 DataFetcher 개발이 완료된 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급한, Member Schema 타입에는 Team 타입이 team필드로 정의되어 있습니다. graphQL의 경우 응답 데이터의 특정 필드만 선택해서 요청할 수 있기 때문에 team 필드를 요청하지 않는 요청들이 존재할 수 있습니다. 만약 team 필드를 요청하지 않았는데 member를 가져오는 과정에서 추가적인 외부 api를 호출해서 team 데이터를 가져오거나, 조인해서 가져오는 경우 불필요한 작업이 될 수 있습니다. 이런 경우, team 필드에 대해 DataFetcher를 추가적으로 등록할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
class MemberController(
    private val memberService: MemberService,
    private val teamService: TeamService,
)  {

    // 리턴하는 Member에는 team을 null로 지정
    @QueryMapping
    suspend fun member(@Argument id: Long, env: DataFetchingEnvironment): Member? {
        return memberService.getMember(id)
    }

    // @SchemaMapping(typeName = &quot;Member&quot;, field = &quot;team&quot;)
    //  @SchemaMapping(typeName = &quot;Member&quot;)
    @SchemaMapping
    suspend fun team(@Argument member: Member, env: DataFetchingEnvironment): Team? {
        if (member.teamId == null) {
            return null
        }
        return teamService.getTeam(member.teamId!!)
    }
}&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;특정 필드에 대한 DataFetcher를 등록하는 경우, @SchemaMapping을 사용합니다. SchemaMapping에는 TypeName과 field 속성이 있습니다. TypeName은 상위 Object 클래스를 의미하고 field는 해당 상위 Object의 필드를 의미합니다. 즉, TypeName 클래스의 field를 가져오는 DataFetcher를 등록한다는 것을 의미합니다. typeName과 field 속성을 생략하는 경우, 인자의 타입이 TypeName이 되고, 함수의 이름이 field가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 경우, Member 타입의 team 필드에 대한 DataFetcher를 등록했기 때문에 member 데이터를 조회하는 dataFetcher에서는 team 필드는 고려하지 않고 null로 리턴해주면 됩니다. 만약 member 데이터를 조회하는 과정에서 team 필드가 요청된 경우, member 메서드가 호출된 이후 team 메서드가 호출되면서 Member의 team 필드를 채워주게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;team필드에 대해 DataFetcher를 등록함으로써, member 데이터 조회 시 불필요한 team 조회를 막을 수 있었습니다. 하지만 여러 명의 Member를 조회하게 되는 경우 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;type Query {
    members(ids: [Long!]!): [Member!]!
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
class MemberController(
    private val memberService: MemberService,
    private val teamService: TeamService,
)  {

    @QueryMapping
    suspend fun members(@Argument ids: Long&amp;lt;Long&amp;gt;, env: DataFetchingEnvironment): List&amp;lt;Member&amp;gt; {
        return memberService.getMembers(ids)
    }

    @SchemaMapping
    suspend fun team(@Argument member: Member, env: DataFetchingEnvironment): Team? {
        if (member.teamId == null) {
            return null
        }
        return teamService.getTeam(member.teamId!!)
    }
}&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;member를 여러명 조회하는 GraphQL API가 추가된 경우, members를 조회하면서 team 필드를 요청하는 경우, members 메서드에서 리턴하는 member 개수만큼 team 메서드가 호출되면서 N+1 문제가 발생합니다. 이를 해결하기 위해 Spring에서는 BatchMapping을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
class MemberController(
    private val memberService: MemberService,
    private val teamService: TeamService,
)  {

    @QueryMapping
    suspend fun members(@Argument ids: Long&amp;lt;Long&amp;gt;, env: DataFetchingEnvironment): List&amp;lt;Member&amp;gt; {
        return memberService.getMembers(ids)
    }

    // @BatchMapping(typeName = &quot;Member&quot;, field = &quot;team&quot;, maxBatchSize = -1)
    // @BatchMapping(typeName = &quot;Member&quot;)
    @BatchMapping
    suspend fun team(members: List&amp;lt;Member&amp;gt;, env: DataFetchingEnvironment): Map&amp;lt;Member, Team?&amp;gt; {
        val teams = teamService.getTeams(members.mapNotNull { it.teamId })
        return members.associateWith { member -&amp;gt;
            teams.firstOrNull { it.id == member.teamId }
        }
    }
}&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;BatchMapping도 SchemaMapping과 같은 속성이 존재합니다. TypeName은 상위 Object 클래스를 의미하고 field는 해당 상위 Object의 필드를 의미합니다. 생략하면, 함수 인자의 타입이 typeName이 되고, field는 함수 명이 됩니다. @BatchMapping은 members 데이터를 조회할 때 team 필드를 요청한 경우, members 함수에서 리턴한 리턴 타입을 함수의 인자로 받고 Map형태의 응답값을 갖습니다. Map 형태는 Key에는 상위 object, value에는 field 타입을 명시해야 합니다. 따라서 리턴할 때, Member에 대응하는 Team을 Map의 Value에 넣어주면 members 메서드에서 리턴한  member 리스트의 각각 team 필드에 데이터를 매핑해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BatchMapping에는 maxBatchSize 옵션으로 배치로 처리할 개수를 지정하여 끊어서 처리할 수 있습니다. -1은 제한을 두지 않는 default값입니다. 만약 members 함수에서 리턴한 member 리스트의 size가 3개인데 maxBatchSize가 2라면 team 함수가 2번 호출되면서 처음에는 메서드의 인자로 member의 개수가 2개인 리스트가, 2번째에는 member의 개수가 1개인 리스트로 끊어서 들어옵니다. 해당 값은 기본값인 -1로 사용하기 보다는 적절한 값을 세팅해주는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 MutationMapping과 BatchMapping을 함께 사용하는 경우입니다. 위의 경우에서 만약 members 메서드가 MutationMapping이었다고 가정하고 MutationMapping 리턴값의 리스트 요소가 5개였다고 가정하면, 마치 maxBatchSize가 1인 것처럼 batchMapping의 인자로 리턴된 리스트 요소가 1개씩 들어가면서 총 5번의 BatchMapping이 호출됩니다. 즉, N+1 문제가 발생합니다. 따라서 MutationMapping의 경우, BatchMapping을 사용하지 않아야만 합니다. 필요한 경우라면 다시 Query로 조회하는 방식을 사용해야만 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 필드에 별도 DataFetcher를 등록해야 하는 경우 SchemaMapping과 BatchMapping 중에 선택하여 사용할 수 있는데 경험상 resource는 단일조회만 있는 경우는 거의 없습니다. 따라서 단일 조회만 고려하여 SchemaMapping을 만들어 사용하기보다는 처음에 만들 때부터 BatchMapping을 만들어두는 것이 더 좋은 선택일 수 있습니다.&lt;/p&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;GraphQLContext&lt;/b&gt;&lt;br /&gt;GraphQLContext는 GraphQL 요청의 실행 중에 공유되는 컨텍스트로, 여러 데이터 페처 간에 공통으로 필요한 데이터를 전달하고, 상태 정보를 유지하며, 유틸리티 메서드를 제공하는 역할을 합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
class MemberController(
    private val memberService: MemberService,
    private val teamService: TeamService,
)  {

    @QueryMapping
    suspend fun members(@Argument ids: Long&amp;lt;Long&amp;gt;, env: DataFetchingEnvironment): List&amp;lt;Member&amp;gt; {
        env.graphQlContext.put(&quot;hello&quot;, &quot;world&quot;)
        return memberService.getMembers(ids)
    }

    @BatchMapping
    suspend fun team(members: List&amp;lt;Member&amp;gt;, context: GraphQLContext): Map&amp;lt;Member, Team?&amp;gt; {
        context.get&amp;lt;String&amp;gt;(&quot;hello&quot;) // world
        val teams = teamService.getTeams(members.mapNotNull { it.teamId })
        return members.associateWith { member -&amp;gt;
            teams.firstOrNull { it.id == member.teamId }
        }
    }
}&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;QueryMapping에서는 DataFetchingEnvironment를 통해 graphqlContext에 값을 넣어주고 BatchMapping에서는 GraphQLContext를 인자로 받아 context에서 값을 꺼내 사용할 수 있습니다. BatchMapping에서는 suspend 키워드를 사용할 경우, 아직 호환이 잘 안 되는지 코루틴 관련 이슈가 발생합니다. 따라서 BatchMapping에서 컨텍스트가 필요한 경우, GraphQLContext를 인자로 받아서 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Schema Inspection &amp;amp; dev tools&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring GraphQL에서는 dev tools로 graphiql 대시보드를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  graphql:
    graphiql:
      enabled: true
    schema:
      inspection:
        enabled: true
    cors:
      allowCredentials: true
      allowedHeaders: &quot;Content-Type&quot;
      allowedMethods: GET, POST, OPTIONS
      allowedOriginPatterns: '*'&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;application.yml 에 graphiql을 enable: true로 지정하고 localhost: port/graphiql로 접속하면 아래와 같은 화면을 확인할 수 있습니다.&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;3056&quot; data-origin-height=&quot;2336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sj9Kx/btsHunTyRcZ/HdQqxCk8Enk6rGsrYdKHT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sj9Kx/btsHunTyRcZ/HdQqxCk8Enk6rGsrYdKHT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sj9Kx/btsHunTyRcZ/HdQqxCk8Enk6rGsrYdKHT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsj9Kx%2FbtsHunTyRcZ%2FHdQqxCk8Enk6rGsrYdKHT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3056&quot; height=&quot;2336&quot; data-origin-width=&quot;3056&quot; data-origin-height=&quot;2336&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;왼쪽 최상단 버튼을 누르면 Docs를 통해 API 명세를 확인할 수 있고, 중앙에 GraphQL API 요청을 보낼 수 있는 화면을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml에 cors 설정도 지정할 수 있으며, inspection을 true(default: true)로 주면 아래와 같은 unmapped 된 schema를 표기해 줍니다. 아래 사진은 schema에 query member가 있지만 실제 @QueryMapping으로 매핑되어 있는 메서드가 누락되었다는 것을 의미합니다.&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;1850&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIHcI6/btsHuJvg8C4/71mz1bUCSj3mom3owD9BgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIHcI6/btsHuJvg8C4/71mz1bUCSj3mom3owD9BgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIHcI6/btsHuJvg8C4/71mz1bUCSj3mom3owD9BgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIHcI6%2FbtsHuJvg8C4%2F71mz1bUCSj3mom3owD9BgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1850&quot; height=&quot;242&quot; data-origin-width=&quot;1850&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Strategy&lt;/h3&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;2548&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvDcKX/btsHtK9L3yj/H1kxGNA92ICZz92RFkYjN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvDcKX/btsHtK9L3yj/H1kxGNA92ICZz92RFkYjN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvDcKX/btsHtK9L3yj/H1kxGNA92ICZz92RFkYjN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvDcKX%2FbtsHtK9L3yj%2FH1kxGNA92ICZz92RFkYjN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2548&quot; height=&quot;1034&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;1034&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;GraphQL 클래스를 타고 들어가 보면 GraphQL은 기본적으로 queryExecutionStrategy는 AsyncExecutionStrategy, mutationExecutionStrategy는 AsyncSerialExecutionStrategy를 사용합니다. 이 두 가지 전략은 GraphQL 쿼리를 실행할 때, 필드들을 어떻게 처리할지 결정합니다.&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;AsyncExecutionStrategy : 비동기적으로 각 필드를 병렬로 실행합니다.&lt;/li&gt;
&lt;li&gt;AsyncSerialExecutionStrategy : 비동기적으로 각 필드를 순차적으로 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;query all($id: Long!) {
  team(id: $id) {
    id
    name
  }
  member(id: $id) {
    id
    name
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 query이기 때문에 AsyncExecutionStrategy가 적용되면서, team과 member는 작성 순서와 관계없이 DataFetcher가 동작합니다. 즉, member가 먼저 수행될 수도, team이 먼저 수행될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;mutation all($id: Long!) {
  team(id: $id) {
    id
    name
  }
  member(id: $id) {
    id
    name
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 mutation의 경우, 작성 순서와 동일한 순서로 DataFetcher가 동작합니다. 즉, team을 먼저 수행하고 나서 완료되어야 member를 수행합니다. 특별히 해당 strategy를 수정할 일은 없지만 nullValue에 대해서 별도의 처리가 필요한 경우, 재정의가 필요할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;type Query {
    members(ids: [Long!]!): [Member!]!
}

type Query {
    members(ids: [Long!]!): [Member!]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체로 query에서 리스트 형태를 반환해야 하는 경우, 요소도 non-null, 리턴 타입도 non-null로 적용하는 것이 일반적입니다. 요소가 없더라도 emptyList를 반환하는 것이 일관성 있는 리턴값일 수 있습니다. 하지만 개발자의 실수로 반환 타입으로 null을 허용한 경우가 발생했다고 가정해 봅시다. 이 경우, 리턴 값이 emptyList가 아닌 null이 리턴되면서 client는 리턴값이 null일 때와 emptyList일 때를 모두 처리해야하는 작업을 해야할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AsyncExecutionStrategy의 메서드 재정의를 통해 이를 해결할 수 있습니다. AsyncExecutionStrategy는 ExecutionStrategy를 상속받아서 구현되어 있는데 ExecutionStrategy에는 아래와 같은 메서드가 존재합니다.&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;3176&quot; data-origin-height=&quot;1062&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y9b09/btsHtHZDKoT/S6VHcyf6uH2Ol2sUeaQepK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y9b09/btsHtHZDKoT/S6VHcyf6uH2Ol2sUeaQepK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y9b09/btsHtHZDKoT/S6VHcyf6uH2Ol2sUeaQepK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy9b09%2FbtsHtHZDKoT%2FS6VHcyf6uH2Ol2sUeaQepK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3176&quot; height=&quot;1062&quot; data-origin-width=&quot;3176&quot; data-origin-height=&quot;1062&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;응답값이 null인 경우 completeValueForNull가 호출되면서 처리가 진행됩니다. 이를 다음과 같이 재정의하여 리턴타입이 List에 해당하는 경우 emptyList를 반환하도록 처리해 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class AsyncExecutionStrategy: AsyncExecutionStrategy() {

    override fun completeValueForNull(
        executionContext: ExecutionContext,
        parameters: ExecutionStrategyParameters
    ): CompletableFuture&amp;lt;ExecutionResult&amp;gt; {
        when (parameters.executionStepInfo.unwrappedNonNullType) {
            // List type
            is GraphQLList -&amp;gt; {
                return CompletableFuture.completedFuture(
                    ExecutionResultImpl(emptyList&amp;lt;Any&amp;gt;(), executionContext.errors),
                )
            }
        }

        return super.completeValueForNull(executionContext, parameters)
    }
}&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;해당 Strategy는 GraphqlSourceBuilderCustomizer를 빈으로 등록해 줌으로써 등록할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class GraphQLConfig {

    /**
     * https://docs.spring.io/spring-graphql/reference/request-execution.html#execution.graphqlsource
     */
    @Bean
    fun graphQLSourceBuilderCustomizer(
    ): GraphQlSourceBuilderCustomizer {

        return GraphQlSourceBuilderCustomizer { sourceBuilder -&amp;gt;
            sourceBuilder.configureGraphQl { builder -&amp;gt;
                builder.queryExecutionStrategy(AsyncExecutionStrategy())
                builder.mutationExecutionStrategy(AsyncSerialExecutionStrategy())
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Operation caching&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL Java 엔진이 쿼리를 실행하기 전에 쿼리를 구문 분석하고 유효성을 검사합니다. 이는 성능에 영향을 미칠 수 있으므로 재구문 분석 및 유효성 검사를 피하기 위해, GraphQL.Builder는 PreparsedDocumentProvider 인스턴스를 사용하여 Document 인스턴스를 재사용할 수 있도록 하는 기능을 제공합니다. &lt;code&gt;이 과정은 쿼리의 결과를 캐시 하는 것이 아니라, 단지 구문 분석된 Document만을 캐시 합니다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class GraphQLConfig {

    @Bean
    fun graphQLSourceBuilderCustomizer(): GraphQlSourceBuilderCustomizer {
        // https://docs.spring.io/spring-graphql/reference/request-execution.html#execution.graphqlsource.operation-caching
        // https://www.graphql-java.com/documentation/execution/#query-caching
        val cache = Caffeine.newBuilder()
            .recordStats()
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .initialCapacity(500)
            .maximumSize(1_000)
            .build&amp;lt;String, PreparsedDocumentEntry&amp;gt;()

        val provider = PreparsedDocumentProvider { executionInput, computeFunction -&amp;gt;
            val mapCompute = Function&amp;lt;String, PreparsedDocumentEntry&amp;gt; { key -&amp;gt; computeFunction.apply(executionInput) }
            cache[executionInput.query, mapCompute]
        }

        return GraphQlSourceBuilderCustomizer { sourceBuilder -&amp;gt;
            sourceBuilder.configureGraphQl { builder -&amp;gt;
                builder.queryExecutionStrategy(AsyncExecutionStrategy())
                builder.mutationExecutionStrategy(AsyncSerialExecutionStrategy())
                builder.preparsedDocumentProvider(provider)
            }
        }
    }
}&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;mapCompute 부분에 디버깅을 찍어보면 같은 요청의 경우, 2번째 요청부터 cache가 hit 된 것을 확인할 수 있습니다. 자세한 내용은 &lt;a href=&quot;https://www.graphql-java.com/documentation/execution/#query-caching&quot;&gt;공식 문서&lt;/a&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;2862&quot; data-origin-height=&quot;1528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DJjvX/btsHtqcPCN7/nlMKukDGh0UPk3teqGZmw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DJjvX/btsHtqcPCN7/nlMKukDGh0UPk3teqGZmw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DJjvX/btsHtqcPCN7/nlMKukDGh0UPk3teqGZmw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDJjvX%2FbtsHtqcPCN7%2FnlMKukDGh0UPk3teqGZmw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2862&quot; height=&quot;1528&quot; data-origin-width=&quot;2862&quot; data-origin-height=&quot;1528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Instrumentation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Instrumentation는 query의 실행을 관찰하고 런타임 동작을 변경할 수 있는 코드를 주입할 수 있습니다. 일종의 interceptor와 유사합니다. graphql에서 제공하는 SimplePerformantInstrumentation를 상속하여 구현할 수 있습니다. 아래는 query요청을 zipkin의 tag로 남기는 instrument입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class RequestLoggingInstrumentation(
    private val httpTracing: HttpTracing,
    private val activeProfile: String,
    private val objectMapper: ObjectMapper,
) : SimplePerformantInstrumentation() {

    private val log = KotlinLogging.logger { }

    override fun beginExecution(
        parameters: InstrumentationExecutionParameters,
        state: InstrumentationState?,
    ): InstrumentationContext&amp;lt;ExecutionResult&amp;gt;? {
        loggingTracingTag(parameters)
        return super.beginExecution(parameters, state)
    }

    private fun loggingTracingTag(
        parameters: InstrumentationExecutionParameters,
    ) {
        try {
            val span = httpTracing
                .tracing()
                .tracer()
                .currentSpan() ?: return

            span.tag(&quot;profile&quot;, activeProfile)

            if (!parameters.query.isNullOrEmpty()) {
                span.tag(&quot;query.string&quot;, parameters.query)
            }

            if (!parameters.operation.isNullOrEmpty()) {
                span.tag(&quot;query.operationName&quot;, parameters.operation)
            }

        } catch (e: Exception) {
            log.warn(&quot;loggingTracingTag error : ${objectMapper.writeValueAsString(parameters.executionInput)}&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Instrumentation 또한 GraphQlSourceBuilderCustomizer를 통해 등록할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class GraphQLConfig {

    @Bean
    fun graphQLSourceBuilderCustomizer(): GraphQlSourceBuilderCustomizer {
        val cache = Caffeine.newBuilder()
            .recordStats()
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .initialCapacity(500)
            .maximumSize(1_000)
            .build&amp;lt;String, PreparsedDocumentEntry&amp;gt;()

        val provider = PreparsedDocumentProvider { executionInput, computeFunction -&amp;gt;
            val mapCompute = Function&amp;lt;String, PreparsedDocumentEntry&amp;gt; { key -&amp;gt; computeFunction.apply(executionInput) }
            cache[executionInput.query, mapCompute]
        }

        return GraphQlSourceBuilderCustomizer { sourceBuilder -&amp;gt;
            sourceBuilder.configureGraphQl { builder -&amp;gt;
                builder.queryExecutionStrategy(AsyncExecutionStrategy())
                builder.mutationExecutionStrategy(AsyncSerialExecutionStrategy())
                builder.preparsedDocumentProvider(provider)
                builder.instrumentation(RequestLoggingInstrumentation(httpTracing, activeProfile, objectMapper))
            }
        }
    }
}&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;graphql에서 별도로 제공되는 Instrumentation는 MaxQueryComplexityInstrumentation와 MaxQueryDepthInstrumentation가 있습니다.&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;MaxQueryDepthInstrumentation : query의 depth를 제한합니다.&lt;/li&gt;
&lt;li&gt;MaxQueryComplexityInstrumentation : query의 복잡도를 제한합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 설정이 없다면 필드 1개당 복잡도는 1로 계산됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;query teams($id: Long!) {
    member(id: $id) {
        id
        name
        modifiedDate
        team {
            id
            name
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리의 경우, 필드가 총 7개이므로 복잡도는 7이고, depth는 3에 해당합니다. 따라서 아래와 같이 등록하여 depth와 complexity를 지정할 수 있습니다. instrumentation을 여러 개 등록할 경우에는 ChainedInstrumentation로 묶어줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;@Configuration
class GraphQLConfig {

    @Bean
    fun graphQLSourceBuilderCustomizer(): GraphQlSourceBuilderCustomizer {
        val cache = Caffeine.newBuilder()
            .recordStats()
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .initialCapacity(500)
            .maximumSize(1_000)
            .build&amp;lt;String, PreparsedDocumentEntry&amp;gt;()

        val provider = PreparsedDocumentProvider { executionInput, computeFunction -&amp;gt;
            val mapCompute = Function&amp;lt;String, PreparsedDocumentEntry&amp;gt; { key -&amp;gt; computeFunction.apply(executionInput) }
            cache[executionInput.query, mapCompute]
        }

        return GraphQlSourceBuilderCustomizer { sourceBuilder -&amp;gt;
            sourceBuilder.configureGraphQl { builder -&amp;gt;
                builder.queryExecutionStrategy(AsyncExecutionStrategy())
                builder.mutationExecutionStrategy(AsyncSerialExecutionStrategy())
                builder.preparsedDocumentProvider(provider)
                builder.instrumentation(
                    ChainedInstrumentation(
                        RequestLoggingInstrumentation(httpTracing, activeProfile, objectMapper),
                        MaxQueryComplexityInstrumentation(100), // 복잡도 
                        MaxQueryDepthInstrumentation(5), // 뎁스 
                    ),
                )
            }
        }
    }
}&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;depth와 complexity를 낮게 설정하는 경우, graphiql에서 제공되는 Docs API 명세가 아래와 같이 노출되지 않는 경우가 있으니 적절히 조절해서 사용해야 합니다.&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;1726&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wih8B/btsHtilFJf3/raLCQLNoorQkjp6oAgTs71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wih8B/btsHtilFJf3/raLCQLNoorQkjp6oAgTs71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wih8B/btsHtilFJf3/raLCQLNoorQkjp6oAgTs71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWih8B%2FbtsHtilFJf3%2FraLCQLNoorQkjp6oAgTs71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1726&quot; height=&quot;846&quot; data-origin-width=&quot;1726&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exception handling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 처리는 MVC와 유사하게 처리됩니다. ControllerAdvice 애노테이션을 클래스에 붙이고 메서드에 @GraphQlExceptionHandler 애노테이션을 붙이면 DataFetcher에서 발생한 예외를 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@ControllerAdvice
class GlobalExceptionHandler {

    @GraphQlExceptionHandler
    fun handleRuntimeException(ex: IllegalArgumentException, env: DataFetchingEnvironment): GraphQLError {
        return GraphqlErrorBuilder.newError()
            .message(ex.message)
            .path(env.executionStepInfo.path)
            .location(env.field.sourceLocation)
            .errorType(ErrorType.BAD_REQUEST)
            .build()
    }

    @GraphQlExceptionHandler
    fun handleRuntimeException(ex: RuntimeException, env: DataFetchingEnvironment): GraphQLError {
        return GraphqlErrorBuilder.newError()
            .message(ex.message)
            .path(env.executionStepInfo.path)
            .location(env.field.sourceLocation)
            .errorType(ErrorType.INTERNAL_ERROR)
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring graphql은 execution ErrorType으로 5개의 타입을 제공하므로 적절히 선택해서 사용할 수 있습니다. 자세한 내용은 &lt;a href=&quot;https://docs.spring.io/spring-graphql/reference/request-execution.html#execution.exceptions&quot;&gt;공식 문서&lt;/a&gt;를 확인 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BAD_REQUEST&lt;/li&gt;
&lt;li&gt;UNAUTHORIZED&lt;/li&gt;
&lt;li&gt;FORBIDDEN&lt;/li&gt;
&lt;li&gt;NOT_FOUND&lt;/li&gt;
&lt;li&gt;INTERNAL_ERROR&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적으로 RuntimeException을 발생시켜 보면 아래와 같은 결과를 보여줍니다.&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;2896&quot; data-origin-height=&quot;1808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvq50U/btsHvJOSA4s/tLJVggjSMOydu6YN52UAy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvq50U/btsHvJOSA4s/tLJVggjSMOydu6YN52UAy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvq50U/btsHvJOSA4s/tLJVggjSMOydu6YN52UAy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcvq50U%2FbtsHvJOSA4s%2FtLJVggjSMOydu6YN52UAy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2896&quot; height=&quot;1808&quot; data-origin-width=&quot;2896&quot; data-origin-height=&quot;1808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;codeGen&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring GraphQL 서버를 구축하려면 GraphQL 명세를 위해 Schema를 작성해야 하며, 이에 따라 매핑되는 클래스와 데이터 클래스들을 만들어줘야 합니다. 명세에 따라 동일한 클래스를 만들어주는 과정은 번거로우며 휴먼 에러가 발생하기 쉬운 작업입니다. 이를 방지하기 위해 Schema 명세대로 클래스를 자동으로 생성해 주는 플러그인을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인을 세팅하기 전에 resources 디렉터리의 트리 구조와 스키마 구조를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;    └── resources
            ├── application.yml
            ├── graphql
             ├── common
              ├── directive.graphqls
              └── scalar.graphqls
             ├── hello
              ├── mutation.graphqls
              └── query.graphqls
             ├── member
              └── member.graphqls
             └── team
                 └── team.graphqls&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;graphql은 namspace가 존재하지 않기 때문에 모든 파일에서 같은 네이밍을 갖는 schema가 존재하면 안 됩니다. 그렇다고 하나의 파일에서 모든 것들을 관리할 수 없으니 각각의 디렉터리로 분리하여 관리하도록 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# mutation.graphqls
type Mutation {
    createHello : String!
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# query.graphqls
type Query {
  hello : String!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 extend 키워드입니다. 각각의 디렉터리에 Query와 Mutation을 정의하기 위해서는 최상위의 Mutation과 Query 타입이 필요합니다. 따라서 이를 처리하기 위해 Dummy에 해당하는 hello 디렉토리에 최상위 Mutation과 Query 타입을 지정해줍니다. 이렇게 정의함으로써 각각의 디렉터리는 extend 키워드를 사용하여 Query와 Mutation을 확장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# member.graphqls
extend type Mutation {
    createMember(input: CreateMemberInput!): Member!
}

extend type Query {
    member(id: Long!): Member
}

type Member {
    id: Long!
    name: String!
#   .. 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 플러그인 세팅을 진행해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;roboconf&quot;&gt;&lt;code&gt;// build.gradle.kts
plugins {
    id(&quot;io.github.kobylynskyi.graphql.codegen&quot;) version &quot;5.10.0&quot;
}

dependencies {
// 생략
}

// graphql
// https://kobylynskyi.github.io/graphql-java-codegen/plugins/gradle/
tasks.named&amp;lt;GraphQLCodegenGradleTask&amp;gt;(&quot;graphqlCodegen&quot;) {

    outputDir = layout.buildDirectory.dir(&quot;generated&quot;).get().asFile
    generatedLanguage = GeneratedLanguage.KOTLIN // 코틀린 언어로 generate
    modelPackageName = &quot;com.example.springgraphql.model&quot; // type, input, enum, union 타입들이 생성되는 패키지
    apiPackageName = &quot;com.example.springgraphql.api&quot; // query, mutation 인터페이스가 생성되는 패키지
    generateApisWithSuspendFunctions = true // query, mutation 인터페이스 메서드에 suspend keyword 추가
    generateDataFetchingEnvironmentArgumentInApis = true // query, mutation 인터페이스 메서드에 마지막 인자에 DataFetchingEnvironment 추가

    apiInterfaceStrategy = ApiInterfaceStrategy.DO_NOT_GENERATE // 개별 인터페이스 생성 X
    apiRootInterfaceStrategy = ApiRootInterfaceStrategy.INTERFACE_PER_SCHEMA // Schema 별 루트 인터페이스 생성
    apiNamePrefixStrategy = ApiNamePrefixStrategy.FOLDER_NAME_AS_PREFIX // api 인터페이스 prefix

    // api 인터페이스 메서드 인자에 @Argument 와 @Valid 애노테이션 추가
    resolverArgumentAnnotations = setOf(&quot;org.springframework.graphql.data.method.annotation.Argument&quot;, &quot;jakarta.validation.Valid&quot;)

    // Query, Mutation, Subscription 
    // https://github.com/kobylynskyi/graphql-java-codegen/issues/983#issue-1280078675
    customAnnotationsMapping = mapOf(
        &quot;^Query\\.\\w+\$&quot; to listOf(&quot;org.springframework.graphql.data.method.annotation.QueryMapping&quot;), // XXQueryXX.fieldName 매칭되는 것들에 @QueryMapping 추가 
        &quot;^Mutation\\.\\w+$&quot; to listOf(&quot;org.springframework.graphql.data.method.annotation.MutationMapping&quot;),
        &quot;^Subscription\\.\\w+$&quot; to listOf(&quot;org.springframework.graphql.data.method.annotation.SubscriptionMapping&quot;),
    )

    // Custom Scalar에 대해 타입 매핑 추가
    // https://github.com/kobylynskyi/graphql-java-codegen/issues/644#issuecomment-932054916
    // https://github.com/kobylynskyi/graphql-java-codegen/issues/1019
    customTypesMapping = mutableMapOf&amp;lt;String, String&amp;gt;(
        &quot;Long&quot; to &quot;Long&quot;,
        &quot;LocalDateTime&quot; to &quot;java.time.LocalDateTime&quot;,
        &quot;LocalDate&quot; to &quot;java.time.LocalDate&quot;,
        &quot;JSON&quot; to &quot;Any&quot;,
    )

    // Schema가 너무 많은 경우, generate token 개수 제한 해제
    // https://github.com/kobylynskyi/graphql-java-codegen/issues/1216
    ParserOptions.setDefaultParserOptions(
        ParserOptions.getDefaultParserOptions().transform { o -&amp;gt; o.maxTokens(Int.MAX_VALUE) },
    )
}

// Automatically generate GraphQL code on project build:
sourceSets {
    getByName(&quot;main&quot;).java.srcDirs(layout.buildDirectory.dir(&quot;generated&quot;).get().asFile)
    getByName(&quot;main&quot;).kotlin.srcDirs(layout.buildDirectory.dir(&quot;generated&quot;).get().asFile)
}

// Add generated sources to your project source sets:
tasks.named&amp;lt;KotlinCompile&amp;gt;(&quot;compileKotlin&quot;) {
    dependsOn(&quot;graphqlCodegen&quot;)
}

tasks.withType&amp;lt;KaptGenerateStubsTask&amp;gt; {
    dependsOn(tasks.named(&quot;graphqlCodegen&quot;))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명은 주석으로 적어두었고 다양한 옵션이 있으므로 &lt;a href=&quot;https://github.com/kobylynskyi/graphql-java-codegen/blob/main/docs/codegen-options.md&quot;&gt;공식 문서&lt;/a&gt;를 확인 바랍니다. 참고로 아직 BatchMapping의 경우는 플러그인에서 지원해주지 않기 때문에 직접 작성해야 합니다.&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;1426&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cG5eeO/btsHuAkUHGG/J2kiSP0ZHWDKsoM2e8trV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cG5eeO/btsHuAkUHGG/J2kiSP0ZHWDKsoM2e8trV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cG5eeO/btsHuAkUHGG/J2kiSP0ZHWDKsoM2e8trV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcG5eeO%2FbtsHuAkUHGG%2FJ2kiSP0ZHWDKsoM2e8trV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1426&quot; height=&quot;1128&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;1128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 세팅하고 빌드하게 되면 위와 같이 build 디렉터리에 schema를 기반으로 클래스들이 자동으로 생성됩니다.&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;3888&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DiAta/btsHtPwkP5y/DMh4XZwGV02UufSzKYE4Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DiAta/btsHtPwkP5y/DMh4XZwGV02UufSzKYE4Sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DiAta/btsHtPwkP5y/DMh4XZwGV02UufSzKYE4Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDiAta%2FbtsHtPwkP5y%2FDMh4XZwGV02UufSzKYE4Sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3888&quot; height=&quot;796&quot; data-origin-width=&quot;3888&quot; data-origin-height=&quot;796&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;생성된 Api Interface를 하나 보면 정규식에 따라 Query에 해당하는 Resolver의 메서드에는 @QueryMapping 애노테이션이 추가되어있고 인자에는 @Valid와 @Argument 애노테이션이 추가되어 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동으로 생성된 클래스를 활용하지 않은 코드와 활용한 코드를 비교해보면 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4618&quot; data-origin-height=&quot;1898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WETrM/btsHul2BoXI/Vng2WtHPR2ibblRqsY2NDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WETrM/btsHul2BoXI/Vng2WtHPR2ibblRqsY2NDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WETrM/btsHul2BoXI/Vng2WtHPR2ibblRqsY2NDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWETrM%2FbtsHul2BoXI%2FVng2WtHPR2ibblRqsY2NDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4618&quot; height=&quot;1898&quot; data-origin-width=&quot;4618&quot; data-origin-height=&quot;1898&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;왼쪽이 플러그인 없이 기존에 작성했던 코드이고, 오른쪽이 자동으로 생성된 데이터 클래스와 인터페이스를 활용한 코드입니다. 언뜻 보면 별차이가 없지만 작업량에는 큰 차이가 있습니다. 왼쪽 코드의 경우, 모든 메서드와 메서드에 사용되는 모든 타입을 개발자가 직접 만들어야 합니다. 하지만 오른쪽의 경우는, 인터페이스를 구현해서 override를 하면 뼈대가 자동으로 만들어질 뿐만 아니라 리턴 타입과 함수의 인자로 사용되는 모든 데이터 타입 클래스가 자동으로 생성됩니다. 따라서 Schema 명세와 다르게 구현되는 휴먼 에러를 배제시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 왼쪽의 경우 개발자가 모든 코드를 작성하다 보니 Schema에는 명시되어 있지만 DataFetcher 구현이 누락된 경우가 발생할 수 있습니다. 앞서, application.yml에 spring.graphql.schema.inspection.enable 값을 true로 주면 application 실행 시점에 매핑되지 않은 Schema를 확인할 수 있지만, 로깅에만 찍히고 application은 정상적으로 띄워지면서 실행 시점에 DataFetcher를 찾지 못하는 런타임 예외가 발생하게 됩니다. 하지만 인터페이스를 구현하게 되면 컴파일 시점에 미구현된 DataFetcher를 확인할 수 있으므로 휴먼 에러를 방지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;validation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring GraphQL은 기본적으로 Spring MVC에서 사용되는 것과 같이 Validation을 지원합니다. 하지만 CodeGen을 사용하면서 자동으로 생성되는 데이터 클래스들에 Validation 애노테이션을 붙여야 하기에 CodeGen 설정에 애노테이션을 붙일 방법이 필요합니다. 이때 Directive를 사용하여 처리할 수 있는데 Directive를 사용하여 Validation을 처리해 주는 라이브러리가 있으므로 기본적인 validation의 경우에는 라이브러리를 활용하여 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;com.graphql-java:graphql-java-extended-validation:22.0&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;@Configuration
class GraphQLConfig {

    @Bean
    fun runtimeWiringConfigurer(): RuntimeWiringConfigurer {

        val validationRules = ValidationRules.newValidationRules()
            .onValidationErrorStrategy(OnValidationErrorStrategy.RETURN_NULL)
            .build()

        return RuntimeWiringConfigurer { builder -&amp;gt;
            builder
                .scalar(ExtendedScalars.GraphQLLong)
                .scalar(ExtendedScalars.Json)
                .scalar(LocalDateTimeScalar.create(null, true, null))
                .scalar(LocalDateScalar.create(null, true, null))
                .directiveWiring(ValidationSchemaWiring(validationRules))
        }
    }
}&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;의존성을 추가하고 ValidationRule을 Schema Directive Wiring 하여 Directive를 통해 validation을 수행하도록 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;# directive.graphqls
directive @NotBlank(message : String = &quot;graphql.validation.NotBlank.message&quot;) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
directive @NotEmpty(message : String = &quot;graphql.validation.NotEmpty.message&quot;) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
directive @Max(value : Int! = 2147483647, message : String = &quot;graphql.validation.Max.message&quot;) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
directive @Min(value : Int! = 0, message : String = &quot;graphql.validation.Min.message&quot;) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
directive @Size(min : Int = 0, max : Int = 2147483647, message : String = &quot;graphql.validation.Size.message&quot;) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/graphql-java/graphql-java-extended-validation?tab=readme-ov-file#the-supplied-directive-constraints&quot;&gt;공식 문서&lt;/a&gt;를 참고하여 필요한 directive를 명세에 등록해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;input CreateMemberInput {
    name: String! @NotBlank
    introduction: String @NotBlank
    requestedBy: String! @NotBlank
#  ...
}&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;그리고 실제 Validation이 필요한 필드에 해당 Directive를 명시하면 validation이 적용됩니다. graphiql를 통해 테스트를 해보면 validation이 적용된 것을 확인할 수 있습니다.&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;3200&quot; data-origin-height=&quot;2200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCcb8Q/btsHu8O01PO/dpCuAkj3A23nrJUqkpA5l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCcb8Q/btsHu8O01PO/dpCuAkj3A23nrJUqkpA5l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCcb8Q/btsHu8O01PO/dpCuAkj3A23nrJUqkpA5l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCcb8Q%2FbtsHu8O01PO%2FdpCuAkj3A23nrJUqkpA5l0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3200&quot; height=&quot;2200&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;2200&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;만약 라이브러리에서 지원하지 않는 custom 한 애노테이션을 만들고자 한다면 directive를 별도로 만들고 codeGen 설정을 통해 추가할 수 있습니다. 만약 notBlank validation을 jakarta.validation.constraints로 사용하고 싶다면 다음과 같이 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;directive @notBlank on INPUT_FIELD_DEFINITION| ARGUMENT_DEFINITION

input CreateMemberInput {
  name: String! @notBlank
  introduction: String @notBlank
  requestedBy: String! @notBlank
  #  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// build.gradle.kts
tasks.named&amp;lt;GraphQLCodegenGradleTask&amp;gt;(&quot;graphqlCodegen&quot;) {

    // 생략..

    // validation
    directiveAnnotationsMapping = mapOf(
        &quot;notBlank&quot; to listOf(&quot;@jakarta.validation.constraints.NotBlank&quot;),
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정 상태로 codeGen을 진행하면 아래와 같이 애노테이션이 붙은 클래스가 생성된 것을 확인할 수 있습니다. 이런 방식으로 custom annotation을 붙일 수 있습니다.&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;1476&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbpEr2/btsHuP90bDU/UHGktJlSOVNMknMcQYCHJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbpEr2/btsHuP90bDU/UHGktJlSOVNMknMcQYCHJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbpEr2/btsHuP90bDU/UHGktJlSOVNMknMcQYCHJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbpEr2%2FbtsHuP90bDU%2FUHGktJlSOVNMknMcQYCHJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1476&quot; height=&quot;850&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;850&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;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-graphql/reference/&quot;&gt;Spring GraphQL 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.graphql-java.com/documentation/getting-started&quot;&gt;GraphQL Java 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://graphql.org/learn/&quot;&gt;GraphQL 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.kakao.com/posts/364&quot;&gt;GraphQL 개념 잡기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>GraphQL</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/88</guid>
      <comments>https://backtony.tistory.com/88#entry88comment</comments>
      <pubDate>Mon, 20 May 2024 08:42:48 +0900</pubDate>
    </item>
    <item>
      <title>Spring R2DBC 다양한 쿼리 사용 방법 (feat. kotlin)</title>
      <link>https://backtony.tistory.com/87</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는 &lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/database/r2dbc/r2dbc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;R2DBC란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC(Reactive Rela tional Database Connectivity)는 드라이버 공급업체가 관계형 데이터베이스에 액세스 하기 위해서 구현한 리액티브 API를 선언하는 API 명세입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC가 만들어진 이유 중 하나는 적은 스레드로 동시성을 처리하기 위해, 더 적은 하드웨어 리소스로 확장할 수 있는 논-블로킹 애플리케이션 스택이 필요해서 입니다. JDBC는 완전한 블로킹 API 이기 때문에, 표준화된 관계형 데이타베이스 액세스 API(즉, JDBC)를 재이용하더라도 이 요구를 충족할 수가 없습니다. ThreadPool를 사용하여 블로킹 동작을 비슷하게 구현할 수도 있지만 이는 제약 사항이 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 결국 논블로킹 데이터베이스 드라이버와도 잘 동작하는 새로운 공통 API를 만드는 계기가 되었고, 적은 스레드와 하드웨어로 더 많은 동시 처리를 할 수 있는 R2DBC가 탄생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactive란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액티브라는 용어는 변화, 가용성, 처리 가능 상태에 반응하는 것을 중심에 두고 만든 프로그래밍 모델을 의미합니다. 논블로킹은 작업을 기다리기보단 완료되거나 데이터를 사용할 수 있게 되면 반응하므로 그런 의미에서 논블로킹은 리액티브라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 리액티브와 연결하는 중요한 메커니즘으로 논-블로킹 백-프레셔가 있습니다. 동기식 명령형 코드에서 블로킹 호출은 호출자를 강제로 기다리게 하는 일종의 백-프레셔입니다. 논블로킹 코드에선 많은 트래픽이 몰릴 경우 문제가 발생하거나 성능이 제대로 나오지 않을 수 있기에 백-프레셔(배압, 역압)을 통해 요청 개수를 제한(프로듀서 속도가 컨슈머 속도를 압도하지 않도록)하여 고가용성을 보장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 멀티스레드로써 병렬처리를 했기에 스레드의 확장만으로는 CPU와 메모리의 제한이 있었지만, 리액티브는 쓰레드를 점유하지 않고 작업을 수행하여 하나의 쓰레드 내에서 동시에 많은 작업을 수행하게 되어 적은 고정된 수의 스레드와 적은 메모리로 최대한의 효율을 내며 확장할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;demo 프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot 3.2.5 버전을 기준으로 demo 프로젝트를 만들고 spring data r2dbc 사용법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세팅&lt;/h3&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;846&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/okfio/btsHct0PAnC/vI6FIklZwaEX7HBwRzBaJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/okfio/btsHct0PAnC/vI6FIklZwaEX7HBwRzBaJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/okfio/btsHct0PAnC/vI6FIklZwaEX7HBwRzBaJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fokfio%2FbtsHct0PAnC%2FvI6FIklZwaEX7HBwRzBaJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;846&quot; height=&quot;357&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;357&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;r2dbc를 사용하기 위해서는 spring webflux가 필요합니다.&lt;/p&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;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
   // kotlin
   implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin&quot;)
   implementation(&quot;io.projectreactor.kotlin:reactor-kotlin-extensions&quot;)
   implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
   implementation(&quot;org.jetbrains.kotlinx:kotlinx-coroutines-reactor&quot;)

   // webflux
   implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)
   kapt(&quot;org.springframework.boot:spring-boot-configuration-processor&quot;)

   // r2dbc
   implementation(&quot;org.springframework.boot:spring-boot-starter-data-r2dbc&quot;)
   implementation(&quot;io.asyncer:r2dbc-mysql:1.1.0&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://r2dbc.io/drivers/&quot;&gt;R2DBC 공식&lt;/a&gt;에 따르면 jasync-sql은 java와 kotlin을 위한 r2dbc wrapper로 postgreSQL와 MySQL을 지원하고 r2dbc-mysql은 MySQL을 지원하는 native driver입니다. 여기서는 r2dbc-mysql을 사용하겠습니다. r2dbc-mysql에 관한 자세한 설명은 &lt;a href=&quot;https://github.com/asyncer-io/r2dbc-mysql&quot;&gt;Github&lt;/a&gt;와 &lt;a href=&quot;https://github.com/asyncer-io/r2dbc-mysql/wiki&quot;&gt;Wiki&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;R2dbcConfiguration&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableR2dbcRepositories 
@EnableR2dbcAuditing
class R2dbcConfiguration&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;@EnableR2dbcRepositories는 r2dbcRepository 구성을 활성화하며, @EnableR2dbcAuditing는 auditing을 활성화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 spring-boot-starter-data-r2dbc 의존성에 의해 별도의 빈 등록이 없으면 필요한 빈들은 application.yml의 값을 읽어서 자동으로 빈으로 등록됩니다. 별도의 수정이 필요한 경우, &lt;a href=&quot;https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#r2dbc.connectionfactory&quot;&gt;AbstractR2dbcConfiguration&lt;/a&gt;을 상속받아 구현할 수 있습니다. 해당 클래스를 구현하면 ConnectionFactory, &lt;a href=&quot;https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#mapping&quot;&gt;r2dbcConverter&lt;/a&gt;, &lt;a href=&quot;https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#mapping.configuration&quot;&gt;R2dbcCustomConversions&lt;/a&gt; 등을 수정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;spring:
  application:
    name: r2dbc

  # https://github.com/asyncer-io/r2dbc-mysql/wiki/getting-started
  r2dbc:
    url: r2dbc:mysql://localhost:3306/r2dbc
    username: root
    password: root
    properties:
      serverZoneId: Asia/Seoul
      # Duration.parse 를 사용하므로 3s 형식으로 넣으면 예외가 발생
      # MySqlConnectionFactoryProvider#setup 메서드 참고
      connectTimeout: PT3S

    # https://github.com/r2dbc/r2dbc-pool
    # https://javadoc.io/doc/io.r2dbc/r2dbc-pool/0.9.0.M1/io/r2dbc/pool/ConnectionPoolConfiguration.Builder.html
    # @See org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties.Pool
    pool:
      initial-size: 10
      max-size: 10
      max-life-time: 30m
      max-create-connection-time: 3s

# https://github.com/asyncer-io/r2dbc-mysql/wiki/logging-statements
logging:
  level:
    io.asyncer.r2dbc.mysql: INFO
    io.asyncer.r2dbc.mysql.QUERY: DEBUG&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring.r2dbc에서는 공통된 설정만을 제공하므로 url, password, username같은 공통된 설정만 적용할 수 있습니다. 이외의 설정은 properties로 자동완성 없이 직접 넣어줘야 하는데 관련 properties는&amp;nbsp;&lt;a href=&quot;https://github.com/asyncer-io/r2dbc-mysql/wiki/getting-started&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다. 이외의 설명은 주석으로 대체하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도메인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;378&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ir3yt/btsHcJWD3Cc/G3OTchdk4aFm8h4C7cenyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ir3yt/btsHcJWD3Cc/G3OTchdk4aFm8h4C7cenyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ir3yt/btsHcJWD3Cc/G3OTchdk4aFm8h4C7cenyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIr3yt%2FbtsHcJWD3Cc%2FG3OTchdk4aFm8h4C7cenyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;114&quot; data-origin-width=&quot;378&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;demo 프로젝트의 도메인은 team과 member 1:N 구성으로 구성됩니다.&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;1082&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NHwrm/btsHeePo8Wk/JNSiSRLscBXmxDcR3GUgcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NHwrm/btsHeePo8Wk/JNSiSRLscBXmxDcR3GUgcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NHwrm/btsHeePo8Wk/JNSiSRLscBXmxDcR3GUgcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNHwrm%2FbtsHeePo8Wk%2FJNSiSRLscBXmxDcR3GUgcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1082&quot; height=&quot;351&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;351&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;demo 프로젝트의 전체적인 구조는 전통적인 layer구조입니다. 특이한 점이 있다면 domain과 dao가 분리되어 있습니다. JPA의 경우에는 lazy 로딩등 여러 기능이 지원되지만 R2DBC에서는 지원되지 않기 때문에 Repository계층에서 Service계층으로 넘겨줄 때, Domain을 다 채워 넣는 형식으로 설계되었습니다.&lt;/p&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;domainDao&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Table(&quot;member&quot;)
data class Member(
    @Id
    val id: Long? = null,
    val name: String,
    val introduction: String?,
    val type: String,
    val teamId: Long?,
    val registeredBy: String,
    @CreatedDate
    val registeredDate: LocalDateTime,
    val modifiedBy: String,
    @LastModifiedDate
    val modifiedDate: LocalDateTime,
)&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;domain&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Member(
    val id: Long? = null,
    val name: String,
    introduction: String? = null,
    val type: Type,
    teamId: Long? = null,
    val registeredBy: String,
    val registeredDate: LocalDateTime = LocalDateTime.now(),
    modifiedBy: String,
    val modifiedDate: LocalDateTime = LocalDateTime.now(),
    private val teamProvider: suspend () -&amp;gt; Team? = { null },
) {

    // 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain과 dao의 차이를 보면 dao의 경우에는 teamId만 가지고 있고, domain의 경우에는 teamProvider이라는 람다를 가지고 있습니다. JPA에서 lazyLoading을 제공하는 것처럼 repository에서 service 계층으로 Member를 만들어서 넘겨줄 때, teamProvider에 team을 가져오는 람다식을 넘겨주고 필요시 해당 teamProvider를 호출하여 JPA와 유사하게 lazy하게 가져오는 방식의 도메인 구성입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;repository&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
interface MemberRepository : CoroutineCrudRepository&amp;lt;Member, Long&amp;gt;, MemberRepositoryCustom&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;repository는 JPA에서 사용하는 것과 비슷하게 CoroutineCrudRepository에서 간단한 쿼리를 제공해 주며, Querydsl 사용방식과 유사하게 Custom 인터페이스를 구현하도록 하여 복잡한 쿼리를 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class MemberRootRepository(
    private val memberRepository: MemberRepository,
    private val teamRootRepository: TeamRootRepository,
) {

    // team db 조회
    suspend fun findById(id: Long): Member? {
        return memberRepository.findById(id)?.let {
            MemberMapper.mapToDomain(it) { it.teamId?.let { teamRootRepository.findById(it) } }
        }
    }

    // 이미 join해서 가져온 경우
    suspend fun findByIdFetch(id: Long): Member? {
        return memberRepository.findByIdFetch(id)
            ?.let { MemberMapper.mapToDomain(it) }
    }
}&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;앞서 언급한 것처럼 리포지토리 계층에서 service로 넘겨줄 때 mapper를 사용하여 dao를 domain으로 변환하게 되는데 teamProvider로 db를 호출하는 repository에서 찾는 방식으로 넘겨줄 수도 있고, member를 조회할 때 team과 join 해서 이미 가져왔다면 해당 team을 바로 넣어주는 형식으로 개발할 수 있습니다. 자세한 코드는 demo 코드를 확인 바랍니다.&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;R2dbcEntityTemplate&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2dbcEntityTemplate은 Spring Data R2DBC에서 제공하는 고수준의 데이터베이스 액세스 추상화 클래스입니다. Spring Data JPA의 JdbcTemplate이나 MongoTemplate과 유사하게 작동하며, R2DBC를 통해 데이터베이스 작업을 수행할 때 편리한 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 간단한 CRUD는 CoroutineCrudRepository를 통해 구현하게 되고, 나머지 복잡한 쿼리의 경우 Querydsl 사용처럼 CustomRepository를 구현하도록 하여 추가적으로 구현하게 되는데 이때 비교적 간단한 쿼리에서는 R2dbcEntityTemplate을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class MemberRepositoryCustomImpl(
    private val template: R2dbcEntityTemplate,
    private val converter: MappingR2dbcConverter
): MemberRepositoryCustom {

    override suspend fun findById(id: Long): Member? {
        val sql = template.select&amp;lt;Member&amp;gt;()
        val whereBuilder = Criteria.where(&quot;id&quot;).`is`(id)

        return sql
            .matching(Query.query(whereBuilder))
            .one() // all or one
            .awaitSingleOrNull()
    }
}&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;sql.matching 이후 one을 체이닝 하면 mono가 반환되고, all을 체이닝하면 flux가 반환됩니다. 코루틴을 사용하므로 awaitSingleOrNull을 사용하여 리액티브 타입을 제거해 줍니다. R2dbcEntityTemplate의 자세한 사용법은 &lt;a href=&quot;https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#r2dbc.entityoperations&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;join&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join과 같이 비교적 복잡한 퀴리의 경우, DatabaseClient를 사용합니다. DatabaseClient는 Spring Data R2DBC에서 제공하는 저수준의 데이터베이스 액세스 클래스입니다. 이를 통해 직접 SQL 쿼리를 작성하고 실행할 수 있습니다. R2dbcEntityTemplate이 내부적으로 DatabaseClient를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class MemberWithTeam(
    val memberId: Long,
    val memberName: String,
    val memberIntroduction: String?,
    val memberType: String,
    val memberRegisteredBy: String,
    val memberRegisteredDate: LocalDateTime,
    val memberModifiedBy: String,
    val memberModifiedDate: LocalDateTime,
    val teamId: Long?,
    val teamName: String?,
    val teamRegisteredBy: String?,
    val teamRegisteredDate: LocalDateTime?,
    val teamModifiedBy: String?,
    val teamModifiedDate: LocalDateTime?,
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class MemberRepositoryCustomImpl(
   private val template: R2dbcEntityTemplate,
   private val converter: MappingR2dbcConverter
): MemberRepositoryCustom {

   override suspend fun findByIdFetch(id: Long): MemberWithTeam? {

      val sql = &quot;&quot;&quot;
         SELECT 
             m.id as member_id,
             m.name as member_name,
             m.introduction as member_introduction,
             m.type as member_type,
             m.team_id as team_id,
             m.registered_by as member_registered_by,
             m.registered_date as member_registered_date,
             m.modified_by as member_modified_by,
             m.modified_date as member_modified_date,
             t.name as team_name,
             t.registered_by as team_registered_by,
             t.registered_date as team_registered_date,
             t.modified_by as team_modified_by,
             t.modified_date as team_modified_date
         FROM member m 
         LEFT JOIN team t on m.team_id = t.id
         WHERE m.id =:memberId
     &quot;&quot;&quot;.trimIndent()

      return template.databaseClient
         .sql(sql)
         .bind(&quot;memberId&quot;, id)
         .map { row, metaData -&amp;gt; converter.read(MemberWithTeam::class.java, row, metaData) }
         .one()
         .awaitSingleOrNull()
   }
}&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;DatabaseClient를 사용하게 되면 쿼리를 직접 작성해야 합니다. 따라서 매핑에 필요한 클래스도 별도로 생성해야 합니다. bind를 통해서 쿼리에 파라미터를 매핑할 수 있고, MappingR2dbcConverter 를 사용하여 row를 객체로 매핑할 수 있습니다. MappingR2dbcConverter에 대해서는 &lt;a href=&quot;https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#mapping&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;one-to-one 혹은 many-to-one 관계의 join의 경우에는 위와 같이 일반적인 형태로 쿼리를 구성하면 됩니다. 하지만 one-to-many의 경우에는 다른 방식의 접근이 필요합니다.&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;766&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DM9AP/btsHecxhShL/it6tZ0EkKX4yw5KSHQJ7Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DM9AP/btsHecxhShL/it6tZ0EkKX4yw5KSHQJ7Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DM9AP/btsHecxhShL/it6tZ0EkKX4yw5KSHQJ7Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDM9AP%2FbtsHecxhShL%2Fit6tZ0EkKX4yw5KSHQJ7Ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;766&quot; height=&quot;621&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;one-to-many의 경우 join을 하면 데이터가 뻥튀기됩니다. 팀 A의 경우 행이 1개이지만 join을 하게 되면 결괏값으로 행이 2개가 나옵니다. JPA의 경우에는 distinct가 이를 해결해 주었지만(hibernate 6.0 버전부터는 default로 distinct가 들어가기 때문에 문제가 해결되었습니다. 자세한 내용은&amp;nbsp;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.0/migration-guide/migration-guide.html#query-sqm-distinct&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다.) r2dbc에서는 별도로 제공되는 것이 없기 때문에 직접 해결해야만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 2가지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;bufferUntilChanged + sort&lt;/li&gt;
&lt;li&gt;groupBy&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;bufferUntilChanged + sort&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9WPdv/btsHbN6znVf/8Vd99FEUEMsUq94Wo4K42K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9WPdv/btsHbN6znVf/8Vd99FEUEMsUq94Wo4K42K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9WPdv/btsHbN6znVf/8Vd99FEUEMsUq94Wo4K42K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9WPdv%2FbtsHbN6znVf%2F8Vd99FEUEMsUq94Wo4K42K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;625&quot; height=&quot;304&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#bufferUntilChanged-java.util.function.Function-&quot;&gt;bufferUntilChanged&lt;/a&gt;는 R2dbc, Spring-data-r2dbc에서 제공하는 것이 아닌 Reactor Flux에서 제공되는 메서드입니다. 위 그림과 같이 인자로 받은 조건에 의해 리스트로 묶어주는 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;team A // 1
team A // 2
team B // 3
team B // 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 위와 같은 결과가 나왔을 때, 1,2가 하나의 리스트, 3,4가 하나의 리스트로 만들어 &lt;code&gt;Flux&amp;lt;List&amp;lt;Team&amp;gt;&amp;gt;&lt;/code&gt; 형태로 반환합니다. 여기서 주의해야 할 점은 기준값이 끊어지는 순간 리스트도 끊어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;team A // 1
team B // 2
team A // 3
team B // 4&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;위와 같은 결과가 나왔을 때는, A가 연속적이지 않기 때문에 1이 하나의 리스트, 2가 하나의 리스트로 만들어지면서 총 4개의 리스트가 만들어집니다. 따라서 이를 방지하기 위해서는 정렬이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class TeamRepositoryCustomImpl(
    private val template: R2dbcEntityTemplate,
    private val converter: MappingR2dbcConverter,
) : TeamRepositoryCustom {

    override suspend fun findAllTeamWithMembers(): List&amp;lt;TeamWithMembers&amp;gt; {
        val sql = &quot;&quot;&quot;
            SELECT 
                m.id as member_id,
                m.name as member_name,
                m.introduction as member_introduction,
                m.type as member_type,
                m.team_id as team_id,
                m.registered_by as member_registered_by,
                m.registered_date as member_registered_date,
                m.modified_by as member_modified_by,
                m.modified_date as member_modified_date,            
                t.name as team_name,
                t.registered_by as team_registered_by,
                t.registered_date as team_registered_date,
                t.modified_by as team_modified_by,
                t.modified_date as team_modified_date
            FROM team t 
            JOIN member m on m.team_id = t.id
            order by t.id
        &quot;&quot;&quot;.trimIndent()

        return template.databaseClient
            .sql(sql)
            .map { row, metaData -&amp;gt; converter.read(TeamWithMemberData::class.java, row, metaData) }
            .all()
            .bufferUntilChanged { it.teamId }
            .map { dataList -&amp;gt;
                TeamWithMembers(
                    team = Team.from(dataList.first()),
                    members = dataList.map { Member.from(it) }
                )
            }
            .collectList()
            .awaitSingle()
    }
}&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;bufferUntilChanged에서 사용되는 기준을 orderBy 절에 추가하여 앞서 언급한 bufferUntilChanged의 문제를 해결하고 나면 결과적으로 map 단계에서 team이 같은 member들에 대한 결괏값들이 list로 나오게 되므로 이를 mapping 하여 one-to-many join을 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class TeamRepositoryCustomImpl(
    private val template: R2dbcEntityTemplate,
    private val converter: MappingR2dbcConverter,
) : TeamRepositoryCustom {

    override suspend fun findAllTeamWithMembers(): List&amp;lt;TeamWithMembers&amp;gt; {

        // 생략

        return template.databaseClient
            .sql(sql)
            .map { row, metaData -&amp;gt; converter.read(TeamWithMemberData::class.java, row, metaData) }
            .all()
            .groupBy { it.teamId }
            .flatMap { groupFlux -&amp;gt;
                groupFlux.collectList().map { dataList -&amp;gt;
                    TeamWithMembers(
                        team = Team.from(dataList.first()),
                        members = dataList.map { Member.from(it) }
                    )
                }
            }
            .collectList()
            .awaitSingle()
    }
}&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;kotlin에서 제공하는 groupBy를 사용할 경우, 비교적 간단하게 기준값으로 묶어서 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;페이징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class MemberRepositoryCustomImpl(
    private val template: R2dbcEntityTemplate,
    private val converter: MappingR2dbcConverter
): MemberRepositoryCustom {

    override suspend fun search(searchCondition: MemberDto.SearchCondition): List&amp;lt;MemberWithTeam&amp;gt; {
        val baseSql = &quot;&quot;&quot;
            SELECT 
                m.id as member_id,
                m.name as member_name,
                m.introduction as member_introduction,
                m.type as member_type,
                m.team_id as team_id,
                m.registered_by as member_registered_by,
                m.registered_date as member_registered_date,
                m.modified_by as member_modified_by,
                m.modified_date as member_modified_date,
                t.name as team_name,
                t.registered_by as team_registered_by,
                t.registered_date as team_registered_date,
                t.modified_by as team_modified_by,
                t.modified_date as team_modified_date
            FROM member m 
            LEFT JOIN team t on m.team_id = t.id
        &quot;&quot;&quot;.trimIndent()

        val whereConditions = generateWhereCondition(searchCondition)

        return template.databaseClient
            .sql(
                baseSql
                    .plusWhere(whereConditions)
                    .plusOrderBy(searchCondition.sort.map { Pair(&quot;m.${it.field}&quot;, Sort.Direction.valueOf(it.sort.name)) })
                    .plusPagination(searchCondition.page, searchCondition.size)
            )
            .bindConditions(whereConditions)
            .bindPage(searchCondition.page, searchCondition.size)
            .map { row, metaData -&amp;gt; converter.read(MemberWithTeam::class.java, row, metaData) }
            .all()
            .collectList()
            .awaitSingle()
    }

    private fun generateWhereCondition(searchCondition: MemberDto.SearchCondition): MutableMap&amp;lt;String, Any?&amp;gt; {
        val whereConditions = mutableMapOf&amp;lt;String, Any?&amp;gt;()
        searchCondition.name?.let { whereConditions[&quot;m.name = :name&quot;] = it }
        searchCondition.type?.let { whereConditions[&quot;m.type = :type&quot;] = it }
        return whereConditions
    }
}&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;페이징 쿼리처럼 동적 쿼리가 필요한 경우에는 아직 지원해 주는 것이 없기 때문에 조건에 따라 sql을 수정하는 작업이 필요합니다. 이를 공통화하기 위해서 util을 만들어서 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun String.plusWhere(conditions: Map&amp;lt;String, Any?&amp;gt;): String {
    if (conditions.isEmpty()) {
        return this
    }

    return &quot;$this\nWHERE ${conditions.keys.joinToString(&quot; AND &quot;)}&quot;
}

fun String.plusGroupBy(vararg columns: String): String {
    return &quot;$this\nGROUP BY ${columns.joinToString(&quot;,&quot;)}&quot;
}

fun String.plusHaving(conditions: Map&amp;lt;String, Any?&amp;gt;): String {
    if (conditions.isEmpty()) {
        return this
    }

    return &quot;$this\nHAVING ${conditions.keys.joinToString(&quot; AND &quot;)}&quot;
}

// 바인딩 지원 X
fun String.plusOrderBy(conditions: List&amp;lt;Pair&amp;lt;String, Sort.Direction&amp;gt;&amp;gt;): String {

    val orderByClause = conditions.joinToString(&quot;, &quot;) { (field, direction) -&amp;gt;
        &quot;${field.toSnakeCase()} ${direction.name}&quot;
    }

    return if (orderByClause.isNotEmpty()) {
        &quot;$this\nORDER BY $orderByClause&quot;
    } else {
        this
    }
}

fun String.toSnakeCase(): String {
    return this.replace(Regex(&quot;([a-z])([A-Z])&quot;), &quot;$1_$2&quot;).lowercase()
}

fun String.plusPagination(page: Int?, size: Int?): String {
    if (page == null &amp;amp;&amp;amp; size == null) {
        return this
    }

    if (page != null &amp;amp;&amp;amp; size != null) {
        return &quot;$this\nLIMIT :page, :size&quot;
    }

    return &quot;$this\nLIMIT :size&quot;
}

fun DatabaseClient.GenericExecuteSpec.bindConditions(
   conditions: Map&amp;lt;String, Any?&amp;gt;,
): DatabaseClient.GenericExecuteSpec {
   val source = conditions.entries.mapNotNull { (condition, value) -&amp;gt;
      val param = Regex(&quot;:(\\w+)&quot;).find(condition)?.value?.removePrefix(&quot;:&quot;)
      if (param == null) {
         null
      } else {
         param to value
      }
   }.toMap()

   return this.bindValues(source)
}

fun DatabaseClient.GenericExecuteSpec.bindPage(
   page: Int,
   size: Int,
): DatabaseClient.GenericExecuteSpec {
   var spec = this
   spec = spec.bind(&quot;page&quot;, (page - 1) * size)
   spec = spec.bind(&quot;size&quot;, size)
   return spec
}&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;order by의 경우, bind를 지원하지 않기 때문에 직접 값을 넣는 방식으로 처리했습니다. where, having 절 같은 경우에는 key에 sql 쿼리를 넣고 value에 실제 바인딩 되는 값을 넣어주도록 만들어두고 util을 통해 sql절을 수정하면서 bind 할 때는 GenericExecuteSpec에 확장함수로 bindConditions을 만들어서 key값에 해당하는 값을 바인딩하는 형식으로 만들었습니다. page의 경우에는 where, having과 공통화하기가 어려워 별도의 bind 확장함수를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class Member(
    val id: Long? = null,
    val name: String,
    introduction: String? = null,
    val type: Type,
    teamId: Long? = null,
    val registeredBy: String,
    val registeredDate: LocalDateTime = LocalDateTime.now(),
    modifiedBy: String,
    val modifiedDate: LocalDateTime = LocalDateTime.now(),
    private val teamProvider: suspend () -&amp;gt; Team? = { null },
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 도메인의 teamProvider에 DB를 조회하는 로직이 있다면, teamProvider를 호출할 때마다 db 조회 쿼리가 발생합니다. 이를 방지하기 위해 한번이라도 teamProvider가 사용된 경우 캐시 해서 항상 동일한 값을 반환하도록 하는 추가적인 작업이 필요할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun &amp;lt;R&amp;gt; memoizeSuspendNullable(func: suspend () -&amp;gt; R?): (suspend () -&amp;gt; R?) {
    var initialized = false
    var result: R? = null

    return {
        if (!initialized) {
            result = func()
            initialized = true
        }
        result
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class Member(
    val id: Long? = null,
    val name: String,
    introduction: String? = null,
    val type: Type,
    teamId: Long? = null,
    val registeredBy: String,
    val registeredDate: LocalDateTime = LocalDateTime.now(),
    modifiedBy: String,
    val modifiedDate: LocalDateTime = LocalDateTime.now(),
    teamProvider: suspend () -&amp;gt; Team? = { null },
) {
    val getTeam = memoizeSuspendNullable { teamProvider() }
}&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;memoizeSuspendNullable를&amp;nbsp;사용하여&amp;nbsp;한&amp;nbsp;번이라도&amp;nbsp;호출된&amp;nbsp;값은&amp;nbsp;저장해두고,&amp;nbsp;이후의&amp;nbsp;반환부터는&amp;nbsp;이전에&amp;nbsp;조회했던&amp;nbsp;것을&amp;nbsp;그대로&amp;nbsp;사용하도록&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;해당&amp;nbsp;값은&amp;nbsp;teamId가&amp;nbsp;변경되어도&amp;nbsp;그대로&amp;nbsp;고정되기&amp;nbsp;때문에&amp;nbsp;만약&amp;nbsp;teamId가&amp;nbsp;수정된다면&amp;nbsp;한번&amp;nbsp;save하고&amp;nbsp;사용해야&amp;nbsp;한다는&amp;nbsp;점을&amp;nbsp;주의해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#get-started:first-steps:what&quot;&gt;spring 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://godekdls.github.io/Spring%20Data%20R2DBC/r2dbcsupport/&quot;&gt;spring 문서 번역&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <category>kotlin</category>
      <category>R2DBC</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/87</guid>
      <comments>https://backtony.tistory.com/87#entry87comment</comments>
      <pubDate>Mon, 6 May 2024 19:43:57 +0900</pubDate>
    </item>
    <item>
      <title>Armeria - zipkin 적용하기</title>
      <link>https://backtony.tistory.com/86</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는&amp;nbsp;&lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는&amp;nbsp;&lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/armeria%20grpc%20graphql/armeria&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MSA 환경과 OpenTracing이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식(monolithic)과 MSA(Micro Service Architecture)에 대해서 간단하게 설명하겠습니다. 모놀리식의 경우 하나의 서버가 서비스의 전반적인 기능을 모두 제공합니다. 그로 인해 복잡도가 증가하고 역할을 나누기 어려운 등 많은 문제가 발생하지만, 클라이언트의 요청을 받으면 하나의 스레드에서 모든 요청을 실행하므로 로그를 확인하기 쉽다는 장점이 있습니다. 그에 반해 MSA의 경우에는 각 서비스의 복잡도가 낮아지고 역할 분담이 용이하지만 클라이언트의 요청을 받았을 때 여러 개의 마이크로 서비스 간에 통신이 발생해 로그를 확인하기 어려운 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위한 방법으로 OpenTracing이 알려져 있습니다. OpenTracing은 간단히 말해 애플리케이션 간 분산 추적을 위한 표준이라고 할 수 있습니다. 이 표준의 대표적인 구현체로 Jaeger와 Zipkin이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분산 추적이란?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIZ1RH/btsHaNTDulj/IUgi3uFEmDQqWFGxh7m47k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIZ1RH/btsHaNTDulj/IUgi3uFEmDQqWFGxh7m47k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIZ1RH/btsHaNTDulj/IUgi3uFEmDQqWFGxh7m47k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIZ1RH%2FbtsHaNTDulj%2FIUgi3uFEmDQqWFGxh7m47k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2082&quot; height=&quot;742&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 요청이 두 개의 서버를 거칠 때 각 서버가 남기는 로그를 연결할 수단이 필요합니다. 먼저 최초로 요청을 받은 서비스에서 traceId를 생성하고 로깅할 때 이 아이디를 함께 기록합니다. 그리고 다른 서비스로 요청이 넘어갈 때 traceId를 함께 전송합니다. traceId를 전파시키는 방법은 다양하지만 대표적으로 HTTP 요청의 경우 헤더에 traceId를 포함한 추적 문맥 정보를 첨부하여 전송합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;zipkin 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Flow&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvhLvB/btsHa23VyGD/zqADkXXiFwbVAkgduIhnN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvhLvB/btsHa23VyGD/zqADkXXiFwbVAkgduIhnN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvhLvB/btsHa23VyGD/zqADkXXiFwbVAkgduIhnN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvhLvB%2FbtsHa23VyGD%2FzqADkXXiFwbVAkgduIhnN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;504&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;504&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;Zipkin의 Flow는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Reporter가 Transport(전송)를 통해서 Collector에 트레이스 정보를 전달합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Transport(전송) 방법으로는 HTTP, Kafka가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전달된 트레이스 정보는 Database에 저장됩니다.&lt;/li&gt;
&lt;li&gt;Zipkin UI에서 API를 통해 해당 정보를 시각화해서 제공합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Components&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zipkin은 4가지 컴포넌트로 구성됩니다.&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;Collector
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;trace 데이터가 collector 데몬에 도착하면 검증 및 저장되며, collector에 의해 조회를 위한 인덱싱이 진행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Storage
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;zipkin은 초기에 Cassandra에 데이터를 저장하기 위해 만들어졌으나 현재는 ES와 MySQL도 지원합니다. &lt;a href=&quot;https://github.com/openzipkin/zipkin#storage-component&quot;&gt;참고&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Query Service
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 저장되고 인덱싱된 후에는 추출하는 방법이 필요합니다. 쿼리 데몬은 추적을 찾고 검색하기 위한 간단한 JSON API를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;WEB UI
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WEB UI는 서비스, 시간 및 주석을 기반으로 추적을 볼 수 있는 방법을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zipkin 헤더&lt;/h3&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;2360&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWbyP4/btsHbeQE0r1/BLGbBEI6k4f9BQnDwOnM3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWbyP4/btsHbeQE0r1/BLGbBEI6k4f9BQnDwOnM3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWbyP4/btsHbeQE0r1/BLGbBEI6k4f9BQnDwOnM3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWbyP4%2FbtsHbeQE0r1%2FBLGbBEI6k4f9BQnDwOnM3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2360&quot; height=&quot;986&quot; data-origin-width=&quot;2360&quot; data-origin-height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zipkin은 &lt;a href=&quot;https://github.com/openzipkin/b3-propagation&quot;&gt;B3-Propagation&lt;/a&gt;을 통해서 OpenTracing을 구현하고 있습니다. B3 propagation은 간단히 말해 'X-B3-'으로 시작하는 X-B3-TraceId와 X-B3-ParentSpanId, X-B3-SpanId, X-B3-Sampled, 이 4개 값을 전달하는 것을 통해서 트레이스 정보를 관리합니다. HTTP를 통해 다른 서버로 전달하는 경우에는 HTTP 헤더를 통해서 전달하고, Kafka 메시지를 통해 전달하는 경우에는 Kafka 헤더를 통해서 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1436&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boGIAU/btsHa2CPK0E/p9oIVghhYPbAe9Br8EzD7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boGIAU/btsHa2CPK0E/p9oIVghhYPbAe9Br8EzD7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boGIAU/btsHa2CPK0E/p9oIVghhYPbAe9Br8EzD7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboGIAU%2FbtsHa2CPK0E%2Fp9oIVghhYPbAe9Br8EzD7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1436&quot; height=&quot;278&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;278&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;A Service가 request를 받아 Trace ID, Span ID를 생성하고 Parent Span ID는 null로 지정됩니다.&lt;/li&gt;
&lt;li&gt;B Service가 A서비스로부터 요청을 받으면 X-B3 헤더를 기반으로 Trace ID는 그대로 이어받고 Span ID는 새롭게 생성하며, Parent Span ID는 A service의 Span ID로 지정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request의 전체 흐름은 Trace ID를 기준으로 트래킹 하며 Span ID로는 해당 Request가 속했던 서비스를 식별이 가능합니다. Parent ID로는 호출 간의 상관관계를 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;706&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EnTwz/btsHbiZQMET/PUCIQfZhUNhIgAU4U6D6z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EnTwz/btsHbiZQMET/PUCIQfZhUNhIgAU4U6D6z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EnTwz/btsHbiZQMET/PUCIQfZhUNhIgAU4U6D6z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEnTwz%2FbtsHbiZQMET%2FPUCIQfZhUNhIgAU4U6D6z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;331&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;331&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;Demo에서는 위와 같은 구조로 Transport 도구로 Kafka를 사용하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버는 계측(instrumented) 라이브러리를 사용해야 Reporter로서 동작할 수 있습니다. Zipkin에서는 다양한 언어에 대한 &lt;a href=&quot;https://zipkin.io/pages/tracers_instrumentation.html&quot;&gt;라이브러리&lt;/a&gt;를 제공하고 있습니다. Java 환경에서는 &lt;a href=&quot;https://github.com/openzipkin/brave&quot;&gt;Brave&lt;/a&gt;를 지원하고 있고 Spring 3.x 이전에는 Spring Cloud Sleuth가 BraveTracer를 통해서 트레이스 데이터를 관리하기 위한 대부분의 기능을 제공했지만 3.x 부터는 지원을 &lt;a href=&quot;https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/&quot;&gt;중단&lt;/a&gt;하고 &lt;a href=&quot;https://micrometer.io/docs/tracing&quot;&gt;Micrometer Tracing&lt;/a&gt;으로 이전되었습니다. spring boot 3.x버전부터는 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.micrometer-tracing&quot;&gt;공식 문서 13.8 Tracing&lt;/a&gt;를 참고하여 다양한 분산 추적 라이브러리와 연동할 수 있습니다. 그리고 Armeria zipkin 연동 방법은 &lt;a href=&quot;https://armeria.dev/docs/advanced-zipkin/&quot;&gt;공식 문서&lt;/a&gt;에서 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;grpc-server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle.kts&lt;/h4&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {

    // 생략

    // zipkin - 필수 의존성
    implementation(&quot;com.linecorp.armeria:armeria-spring-boot3-actuator-starter:$armeriaVersion&quot;)
    implementation(&quot;com.linecorp.armeria:armeria-brave:$braveVersion&quot;)
    implementation(&quot;io.micrometer:micrometer-tracing-bridge-brave&quot;)
    implementation(&quot;io.zipkin.reporter2:zipkin-reporter-brave&quot;)

    // zipkin - kafka 전송을 위한 optional 의존성
    implementation(&quot;io.zipkin.reporter2:zipkin-sender-kafka&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;application.yml&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 생략 ..
management:
  tracing:
    enabled: true
    sampling:
      probability: 1.0
    propagation:
      type: B3

zipkin:
  endpoint: http://localhost:19092
  messageTimeout: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/&quot;&gt;probability&lt;/a&gt;를 1.0으로 지정하며 모두 샘플링 하도록 하였고, propagation은 B3로 지정했습니다. propagation은 B3, B3_MULTI, W3C 방식을 제공하는데 B3는 헤더를 하나로 뭉쳐서 보내는 방식이고 multi는 각 헤더를 별개로 나눠서 보내는 방식입니다. 자세한 내용은 &lt;a href=&quot;https://github.com/openzipkin/b3-propagation#multiple-headers&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ArmeriaConfig&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class ArmeriaConfig {

    // 생략

    @Bean
    fun armeriaServerConfigurator(
        grpcService: GrpcService,
        tracing: Tracing
    ): ArmeriaServerConfigurator {
        return ArmeriaServerConfigurator {            
            // 생략

            it.service(grpcService, BraveService.newDecorator(tracing))
        }
    }
}&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;Armeria 설정은 grpcService를 등록할 때, Armeria에서 제공하는 BraveService 데코레이터를 추가해주면 끝입니다.&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;ZipkinConfiguration&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import com.linecorp.armeria.common.brave.RequestContextCurrentTraceContext;

import brave.Tracing;
import brave.http.HttpTracing;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;

AsyncZipkinSpanHandler spanHandler = ...
Tracing tracing = Tracing.newBuilder()
                         .localServiceName(&quot;myService&quot;)
                         .currentTraceContext(RequestContextCurrentTraceContext.ofDefault())
                         .addSpanHandler(spanHandler)
                         .build();
HttpTracing httpTracing = HttpTracing.create(tracing);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://armeria.dev/docs/advanced-zipkin/&quot;&gt;공식 문서&lt;/a&gt;에서 제공하는 Armeria zipkin 연동방식은 위와 같습니다. 눈여결 볼 점은 RequestContextCurrentTraceContext입니다. Armeria는 내부에서 요청을 처리할 때 여러 스레드를 거칠 수 있습니다. 때문에 로깅할 때 이 점을 고려하여 slf4j.MDC를 관리해주어야 합니다. 추적 문맥 역시 요청을 처리하다 스레드가 바뀌면 정보를 유지해야 합니다. Armeria는 여러 스레드를 걸친 요청 문맥 정보(추적 문맥 포함)를 RequestContext에 넣어 저장합니다. RequestContextCurrentTraceContext는 추적 문맥을 RequestContext에 저장하고 자동으로 추적 문맥을 로드하는 기능을 제공하여 비동기프로그래밍에서 threadLocal를 사용하면서 발생하는 문제를 회피할 수 있는 역할을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 spring을 사용하지 않은 예시이므로 spring을 활용해봅시다.&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;currentTraceContext를 armeria에서 제공하는 RequestContextCurrentTraceContext로 변경&lt;/li&gt;
&lt;li&gt;추적 데이터 kafka로 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring은 BraveAutoConfiguration 클래스에 의해 기본적으로 추적에 필요한 대부분의 빈들이 자동으로 등록되므로 위와 같은 부분만 수정해 주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;/**
 * @see org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration
 */
@Configuration
class ZipkinConfiguration(
    private val zipkinProperties: ZipkinProperties,
) {

    private val log = KotlinLogging.logger { }

    @Bean
    fun reporter(sender: Sender): AsyncReporter&amp;lt;Span&amp;gt; {
        return AsyncReporter.builder(sender).build(SpanBytesEncoder.PROTO3)
    }

    @Bean
    fun sender(): Sender {
        return KafkaSender.newBuilder()
            .encoding(Encoding.PROTO3)
            .bootstrapServers(zipkinProperties.endpoint)
            // kafkaConfig properties 수정하는 경우 사용
            // .overrides()
            .build()
    }

    /**
     * @see https://armeria.dev/docs/advanced-zipkin/
     * @see org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration.braveTracing
     * @see org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration.braveCurrentTraceContext
    */
    @Bean
    fun braveCurrentTraceContext(
        scopeDecorators: List&amp;lt;CurrentTraceContext.ScopeDecorator&amp;gt;,
        currentTraceContextCustomizers: List&amp;lt;CurrentTraceContextCustomizer&amp;gt;,
    ): CurrentTraceContext {

        val builder = RequestContextCurrentTraceContext.builder()
        scopeDecorators.forEach(
            Consumer { scopeDecorator: CurrentTraceContext.ScopeDecorator -&amp;gt;
                builder.addScopeDecorator(
                    scopeDecorator,
                )
            },
        )
        for (currentTraceContextCustomizer in currentTraceContextCustomizers) {
            currentTraceContextCustomizer.customize(builder)
        }

        return builder.build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;client&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle.kts&lt;/h4&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    // 생략

    // for zipkin
    implementation(&quot;org.springframework.boot:spring-boot-starter-actuator&quot;)
    implementation(&quot;io.micrometer:micrometer-tracing-bridge-brave&quot;)
    implementation(&quot;io.zipkin.reporter2:zipkin-reporter-brave&quot;)
    implementation(&quot;io.zipkin.reporter2:zipkin-sender-kafka&quot;)
    implementation(&quot;io.zipkin.brave:brave-instrumentation-grpc&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grpc-server와 같은 의존성에서 instrumentation-grpc가 추가적으로 들어갔습니다. 이는 stub으로 grpc-server를 호출할 때, 추적 문맥을 추가하는 기능을 제공합니다. application.yml설정은 grpc-server와 동일합니다.&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;zipkinConfiguration&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;/**
 * @see org.springframework.boot.actuate.autoconfigure.tracing.zipkin
 */
@Configuration
class ZipkinConfiguration(
    private val zipkinProperties: ZipkinProperties,
) {

    private val log = KotlinLogging.logger { }

    @Bean
    fun grpcTracing(tracing: Tracing): GrpcTracing {
        return GrpcTracing.create(tracing)
    }

    @Bean
    fun reporter(sender: Sender): AsyncReporter&amp;lt;Span&amp;gt; {
        return AsyncReporter.builder(sender).build(SpanBytesEncoder.PROTO3)
    }

    @Bean
    fun sender(): Sender {
        return KafkaSender.newBuilder()
            .encoding(Encoding.PROTO3)
            .bootstrapServers(zipkinProperties.endpoint)
            // kafkaConfig properties 수정하는 경우 사용
            // .overrides()
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grpc-server의 zipkinConfiguration과 똑같지만 armeria를 사용하지 않으므로 RequestContextCurrentTraceContext 관련 설정은 할 수 없습니다. 이와 관련된 해결책으로는 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/issues/33372#issuecomment-1443766925&quot;&gt;이슈&lt;/a&gt;를 참고하여 Application에 다음과 같이 hook을 추가해 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@ConfigurationPropertiesScan
@SpringBootApplication
class RestClientApplication

fun main(args: Array&amp;lt;String&amp;gt;) {
    runApplication&amp;lt;RestClientApplication&amp;gt;(*args)
    Hooks.enableAutomaticContextPropagation()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GrpcChannelConfig&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Configuration
class GrpcChannelConfig(
    private val grpcTracing: GrpcTracing,
    private val grpcProperties: GrpcProperties,
) {

    @Bean
    fun grpcChannel(): ManagedChannel {

        // 생략
        val builder = NettyChannelBuilder.forAddress(grpcProperties.endpoint, grpcProperties.port)
            .intercept(grpcTracing.newClientInterceptor())
            .negotiationType(NegotiationType.TLS)

        // 생략

        return builder.build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Channel을 만들 때, 앞서 생성해 준 grpcTracing을 주입받아서 interceptor로 추가해 줍니다. 해당 인터셉터는 타 서비스의 호출 간의 추적 문맥을 이어주는 역할을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 stub이 아닌 일반적인 restTemplate 혹은 webClient를 사용하는 경우라면, RestTemplateBuilder, WebClientBuilder를 사용하여 인스턴스를 생성하면 추가적인 인터셉터 등록 없이 사용할 수 있습니다. 자세한 내용은 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.micrometer-tracing.propagating-traces&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zipkin 설치&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# docker-compose -f docker-compose.yml up

version: '3.7'

services:
  storage:
    image: ghcr.io/openzipkin/zipkin-elasticsearch8:${TAG:-latest}
    container_name: elasticsearch
    ports:
      - 9200:9200

  kafka:
    image: ghcr.io/openzipkin/zipkin-kafka:${TAG:-latest}
    container_name: kafka
    ports:
      - 19092:19092

  zipkin:
    image: ghcr.io/openzipkin/zipkin:${TAG:-latest}
    container_name: zipkin
    ports:
      - 9411:9411
    environment:
      - STORAGE_TYPE=elasticsearch
      - ES_HOSTS=elasticsearch:9200
      - KAFKA_BOOTSTRAP_SERVERS=kafka:9092
    depends_on:
      storage:
        condition: service_healthy
      kafka:
        condition: service_healthy&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;위 docker-compose 구성은 es, kafka, zipkin을 띄우고, zipkin은 kafka에서 데이터를 수급하여 es에 저장하는 형태입니다. zipkin github에서 제공되는 &lt;a href=&quot;https://github.com/openzipkin/zipkin/tree/master/docker/examples&quot;&gt;샘플&lt;/a&gt;을 참고하여 작성하였고, 추가적인 옵션은 &lt;a href=&quot;https://github.com/openzipkin/zipkin/blob/master/zipkin-server/README.md&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;web ui&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5104&quot; data-origin-height=&quot;2254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GyRnd/btsHaYgeIxJ/Zq5H7qd8gyEl5FsccQzyx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GyRnd/btsHaYgeIxJ/Zq5H7qd8gyEl5FsccQzyx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GyRnd/btsHaYgeIxJ/Zq5H7qd8gyEl5FsccQzyx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGyRnd%2FbtsHaYgeIxJ%2FZq5H7qd8gyEl5FsccQzyx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5104&quot; height=&quot;2254&quot; data-origin-width=&quot;5104&quot; data-origin-height=&quot;2254&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;여기까지 세팅으로 client가 grpc-server를 호출하는 api를 호출하고 localhost:9411로 접속해 보면 zipkin에서 제공하는 web ui로 추적을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kafka&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;actor -&amp;gt; client -&amp;gt; grpc-server -&amp;gt; kafka -&amp;gt; consumer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 위와 같은 형태의 요청으로 추적이 잘 되는지 확인해 보겠습니다. 자세한 내용은 &lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/kafka/micrometer.html&quot;&gt;문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 consumer batch listener의 경우 지원하지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;grpc-server&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.kafka:spring-kafka&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CommonKafkaProducerConfig&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class CommonKafkaProducerConfig {

    // 생략
    @Bean
    fun commonKafkaTemplate(): KafkaTemplate&amp;lt;String, Any&amp;gt; {
        return KafkaTemplate(commonProducerFactory()).apply {
            setObservationEnabled(true) // zipkin
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;consumer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;consumerConfig&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@EnableKafka
@Configuration
class ConsumerConfig {

    // 생략

    @Bean(COMMON)
    fun commonKafkaListenerContainerFactory(
        commonConsumerFactory: ConsumerFactory&amp;lt;String, Any&amp;gt;,
        commonErrorHandler: CommonErrorHandler,
    ): KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, Any&amp;gt;&amp;gt; {
        return ConcurrentKafkaListenerContainerFactory&amp;lt;String, Any&amp;gt;().apply {
            consumerFactory = commonConsumerFactory
            containerProperties.isObservationEnabled = true // zipkin
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;web ui&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5094&quot; data-origin-height=&quot;2124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2JBs6/btsHan1RVvU/aKTUXsX72pW2neXzKkHhq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2JBs6/btsHan1RVvU/aKTUXsX72pW2neXzKkHhq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2JBs6/btsHan1RVvU/aKTUXsX72pW2neXzKkHhq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2JBs6%2FbtsHan1RVvU%2FaKTUXsX72pW2neXzKkHhq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5094&quot; height=&quot;2124&quot; data-origin-width=&quot;5094&quot; data-origin-height=&quot;2124&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;여기까지 세팅으로 호출하고 web ui로 확인해 보면 잘 추적되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트러블슈팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kafka로 데이터를 전송하기 전에 app이 종료되는 경우&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4756&quot; data-origin-height=&quot;1052&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFs6V/btsHa1KLfnf/wGRizWZtIdKC3ifOLXfGOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFs6V/btsHa1KLfnf/wGRizWZtIdKC3ifOLXfGOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFs6V/btsHa1KLfnf/wGRizWZtIdKC3ifOLXfGOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFs6V%2FbtsHa1KLfnf%2FwGRizWZtIdKC3ifOLXfGOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4756&quot; height=&quot;1052&quot; data-origin-width=&quot;4756&quot; data-origin-height=&quot;1052&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;app을 종료할 때, kafka로 데이터를 전송하기 전에 종료되는 경우 위와 같은 예외가 발생하여 아래와 같이 코드를 수정하여 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class ZipkinConfiguration(
    private val zipkinProperties: ZipkinProperties,
) {

    private val log = KotlinLogging.logger { }

    @Bean
    fun reporter(sender: Sender): AsyncReporter&amp;lt;Span&amp;gt; {
        val reporter = AsyncReporter.builder(sender).build(SpanBytesEncoder.PROTO3)

        // zipkin에 전송하기 전에 app이 종료되는 경우 방지
        // https://github.com/openzipkin/zipkin-reporter-java/issues/202
        // https://github.com/spring-cloud/spring-cloud-sleuth/blob/3.1.x/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinAutoConfiguration.java#L136
        Runtime.getRuntime().addShutdownHook(
            Thread {
                log.info { &quot;Flushing remaining spans on shutdown&quot; }
                reporter.flush()
                try {
                    Thread.sleep(TimeUnit.SECONDS.toMillis(zipkinProperties.messageTimeout) + 500)
                    log.debug { &quot;Flushing done - closing the reporter&quot; }
                    reporter.close()
                } catch (e: Exception) {
                    throw IllegalStateException(e)
                }
            },
        )

        return reporter
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빈 초기화 순서 문제로 수집이 안 되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObservationRegistryPostProcessor와 ObservationRegistry는 Spring Boot 3에서 모니터링과 관찰 관련 기능을 효율적으로 관리하기 위해 사용됩니다. ObservationRegistry는 애플리케이션에서 발생하는 다양한 이벤트와 메트릭을 관찰하고 기록하는 중앙 집중식 저장소 역할을 하고, ObservationRegistryPostProcessor는 BeanPostProcessor의 구현체로, Spring 컨테이너의 빈 생명주기 중 특정 시점에 개입하여 ObservationRegistry 빈에 추가적인 설정이나 초기화 작업을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 빈 설정이 없다면 ObservationRegistryPostProcessor, ObservationRegistry 순으로 초기화됩니다. 하지만 특정 빈 등록으로 인해 이 순서가 꼬일 수 있습니다. 경험상으로는 MethodSecurityExpressionHandler를 static bean으로 등록하면서 둘의 순서가 꼬이게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObservationRegistryPostProcessor가 자동으로 ObservationRegistry를 후처리 하기 전에 ObservationRegistry가 초기화되어 버린 경우 아래와 같이 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Configuration
class ObservationConfig(
    private val observationRegistryPostProcessor: BeanPostProcessor,
    private val observationRegistry: ObservationRegistry,
) {

    @PostConstruct
    fun postProcess() {
        observationRegistryPostProcessor.postProcessAfterInitialization(observationRegistry, &quot;observationRegistry&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 ObservationRegistry가 초기화된 후 필요한 설정이 적용되지 않았을 때, @PostConstruct를 통해 이를 보정하고, 모든 관련 설정이 적절하게 적용되도록 하는 해결책을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://zipkin.io/&quot;&gt;zipkin 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/line-ads-msa-opentracing-zipkin&quot;&gt;LINE 광고 플랫폼의 MSA 환경에서 Zipkin을 활용해 로그 트레이싱하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.micrometer-tracing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://armeria.dev/docs/advanced-zipkin/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Armeria 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Armeria</category>
      <category>armeria</category>
      <category>opentracing</category>
      <category>spring</category>
      <category>zipkin</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/86</guid>
      <comments>https://backtony.tistory.com/86#entry86comment</comments>
      <pubDate>Sat, 4 May 2024 17:28:00 +0900</pubDate>
    </item>
    <item>
      <title>Armeria gRPC 서버에 mTLS 적용하기</title>
      <link>https://backtony.tistory.com/85</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는&amp;nbsp;&lt;a href=&quot;https://backtony.tistory.com&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는&amp;nbsp;&lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/armeria%20grpc%20graphql/armeria&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;mTLS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mTLS는 TLS의 확장된 형태로, TLS는 서버의 신원만을 인증하는 반면 mTLS는 서버뿐만 아니라 클라이언트도 자신의 신원을 인증서를 통해 증명해야 하는 상호 인증 방식입니다. 이는 양방향 인증이라고도 하며, 서로의 인증서를 검증함으로써 보다 강화된 보안을 제공합니다.&lt;/p&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;TLS 통신 과정&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxXC3v/btsG8R1eVoB/VPnO1UcErtxd2QaoP2DVc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxXC3v/btsG8R1eVoB/VPnO1UcErtxd2QaoP2DVc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxXC3v/btsG8R1eVoB/VPnO1UcErtxd2QaoP2DVc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxXC3v%2FbtsG8R1eVoB%2FVPnO1UcErtxd2QaoP2DVc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1394&quot; height=&quot;540&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;540&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;클라이언트가 서버에 연결&lt;/li&gt;
&lt;li&gt;서버가 TLS 인증서를 제시&lt;/li&gt;
&lt;li&gt;클라이언트가 서버의 인증서를 확인&lt;/li&gt;
&lt;li&gt;클라이언트와 서버가 암호화된 TLS 연결을 통해 정보를 교환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mTLS 통신 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 TLS에서 서버에는 TLS 인증서와 공개/개인 키 쌍이 있지만 클라이언트에는 없습니다. 그러나 mTLS에서는 클라이언트와 서버 모두에 인증서가 있고 양측 모두 공개/개인 키 쌍을 사용하여 인증합니다. 일반 TLS와 비교하여 mTLS에는 양 당사자를 확인하기 위한 추가 단계가 있습니다.&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;1344&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceAymP/btsG9xOKDSP/es5aMx8TdMbedC2c4Pc4wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceAymP/btsG9xOKDSP/es5aMx8TdMbedC2c4Pc4wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceAymP/btsG9xOKDSP/es5aMx8TdMbedC2c4Pc4wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceAymP%2FbtsG9xOKDSP%2Fes5aMx8TdMbedC2c4Pc4wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1344&quot; height=&quot;544&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;544&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;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;서버가 TLS 인증서를 제시&lt;/li&gt;
&lt;li&gt;클라이언트가 서버의 인증서를 확인&lt;/li&gt;
&lt;li&gt;클라이언트가 TLS 인증서를 제시&lt;/li&gt;
&lt;li&gt;서버가 클라이언트의 인증서를 확인&lt;/li&gt;
&lt;li&gt;서버가 액세스 권한을 부여&lt;/li&gt;
&lt;li&gt;클라이언트와 서버가 암호화된 TLS 연결을 통해 정보를 교환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mTLS는 조직 내의 사용자, 장치, 서버를 확인하기 위해 Zero Trust 보안 프레임워크에서 자주 사용됩니다. API 엔드포인트를 확인하여 승인되지 않은 당사자가 잠재적으로 악의적인 API 요청을 보낼 수 없도록 하여 API를 안전하게 유지하는 데에도 도움이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zero Trust는 사용자, 장치, 네트워크 트래픽이 기본적으로 신뢰할 수 없음을 의미하며, 이는 많은 보안 취약점을 제거하는 데 도움이 되는 접근 방식입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenSSL 사설 인증서 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설 인증서를 만들기 전에 알아두어야할 용어는 다음과 같습니다.&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;Self Signed Certificate(SSC)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 인증서는 개인키 소유자의 공개키를 인증기관(CA)에 전달하면 인증기관에서는 전달받은 공개키와 기타 정보를 사용하여 인증기관의 개인키로 암호화하여 인증서를 만들게 됩니다. 즉, 인증서는 개인키 소유자의 공개키(public key)에 인증기관의 개인키로 서명한 데이터입니다. 따라서 모든 인증서는 발급기관(CA)이 있어야 합니다. 하지만 최상위에 있는 인증기관(root ca)은 서명해 줄 상위 인증기관이 없으므로 root ca의 개인키로 스스로의 인증서에 서명하여 최상위 인증기관 인증서를 만들게 됩니다. 이렇게 스스로 서명한 root ca 인증서를 Self Signed Certificate(SSC)라고 합니다.&lt;/li&gt;
&lt;li&gt;IE, FireFox, Chrome 등의 Web Browser 제작사는 VeriSign 이나 comodo 같은 유명 ROOT CA 들의 인증서를 신뢰하는 CA로 브라우저에 미리 탑재해 놓습니다. 위와 같은 기관에서 발급된 SSL 인증서를 사용해야 browser에서는 해당 SSL 인증서를 신뢰할 수 있는데 OpenSSL로 만든 ROOT CA와 SSL 인증서는 Browser가 모르는 기관이 발급한 인증서이므로 보안 경고를 발생시키게 되나 내부 서버 간의 통신 용도 혹은 테스트 사용 시에는 지장이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Certificate Signing Request(CSR)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSR(Certificate Signing Request) 은 인증기관에 인증서 발급 요청을 하는 특별한 ASN.1 형식의 파일이며(PKCS#10 - RFC2986) 그 안에는 내 공개키 정보와 사용하는 알고리즘 정보등이 들어 있습니다. 개인키는 외부에 유출되면 안 되므로 특별한 형식의 파일을 만들어서 인증기관에 전달하여 인증서를 발급받습니다. 쉽게 말하자면, CSR은 인증기관에 내 인증서를 만들어달라는 인증서 발급 신청서라고 볼 수 있습니다.&lt;/li&gt;
&lt;li&gt;SSL 인증서 발급시 CSR 생성은 Web Server에서 이루어지는데 Web Server 마다 방식이 상이하여 사용자들이 CSR 생성등을 어려워하니 인증서 발급 대행 기관에서 개인키까지 생성해서 보내주고는 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증서 생성 순서&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;자체 서명 루트 인증서 생성 (ca.crt)&lt;/li&gt;
&lt;li&gt;서버 인증서 키 생성 (server.key)&lt;/li&gt;
&lt;li&gt;서버 인증서 CSR 생성 (server.csr)&lt;/li&gt;
&lt;li&gt;서버 인증서 생성 및 CA루트 키로 서명&lt;/li&gt;
&lt;li&gt;클라이언트 인증서 키 생성 (client.key)&lt;/li&gt;
&lt;li&gt;클라이언트 인증서 CSR 생성 (client.csr)&lt;/li&gt;
&lt;li&gt;클라이언트 인증서 생성 및 CA루트 키로 서명&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인증서 생성 config 파일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서를 만들때 openssl 명령어를 사용하는데 명령어로 모든 설정을 명시하기 번거로우므로 보통 config 파일을 만들어 두고 명령어에 사용합니다. config 파일은 대괄호([])로 구성된 여러 섹션으로 구성되며 섹션 내에서는 key/value 형태로 값이 할당됩니다. 같은 섹션에서 key가 반복되면 마지막 값을 제외한 모든 값들이 무시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# req 섹션은 openSSL의 예약되어 있는 섹션명으로 config 파일로 명시할 경우, 
# CSR을 만드는 명령어일때 참조됩니다.
# 예약된 명령어 참조 : https://www.openssl.org/docs/man3.0/man1/openssl.html
[ req ]
# 생성될 키의 크기를 비트 단위로 지정
default_bits       = 2048
# CSR에 포함될 주체(Subject)의 정보를 입력받는 섹션의 이름을 지정
distinguished_name = req_distinguished_name
# SR 생성 시 적용할 추가 확장 설정을 포함하는 섹션의 이름을 지정
req_extensions     = req_ext

# 구체적인 주체 정보 필드(예: 국가명, 조직명, 공통 이름 등)를 정의
[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = KR
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = Gyeonggi-do
localityName                = Locality Name (eg, city)
localityName_default        = Seongnam-si
organizationName            = Organization Name (eg, company)
organizationName_default    = junseong
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = localhost

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = localhost
IP.1 = 127.0.0.1

# 아래는 CA, 서버, client 인증서를 만들때 사용할 커스텀 섹션입니다.
# 설정 참조 
#   https://www.openssl.org/docs/man3.0/man5/x509v3_config.html
#   https://superuser.com/questions/738612/openssl-ca-keyusage-extension
# 사용자 커스텀 섹션으로 적용하기 위해서는 명령어에 -extension 으로 명시해야 합니다.

# 인증서가 CA(인증기관)로서 작동해야할 때 사용되는 extension
[ v3_ca ]
basicConstraints = critical,CA:TRUE
keyUsage = digitalSignature,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
subjectAltName = @alt_names

# 사용자 커스텀 섹션으로 적용하기 위해서는 명령어에 -extension 으로 명시해야 합니다.
# 인증서가 client, server 인증서로 사용되며 하위 인증서를 발행할 권한로서 작동해야할 때 사용되는 extension
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth,clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier  = keyid:always,issuer:always
subjectAltName = @alt_names&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;basicConstraints : X.509 인증서에 대한 확장으로 인증서가 CA로 사용될 수 있을지 여부를 결정합니다.&lt;/li&gt;
&lt;li&gt;keyUsage : X.509의 확장 필드로 인증서에 포함된 공개 키가 어떤 목적으로 사용될 수 있을지 명시합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;digitalSignature : 공개 키를 사용하여 디지털 서명을 생성하고 검증할 수 있음을 나타냅니다. 이는 데이터의 무결성과 발신자의 인증을 보장하는 데 사용됩니다.&lt;/li&gt;
&lt;li&gt;keyCertSign : 인증서가 다른 인증서에 대한 서명을 할 수 있음을 나타냅니다.&lt;/li&gt;
&lt;li&gt;cRLSign : 인증서 폐지 목록(Certificate Revocation List, CRL)에 서명할 수 있음을 나타냅니다.&lt;/li&gt;
&lt;li&gt;nonRepudiation : 데이터의 송신자가 나중에 데이터를 보냈다는 사실을 부인하는 것을 방지하는 데 사용됩니다.&lt;/li&gt;
&lt;li&gt;keyEncipherment : 공개 키를 사용하여 세션 키와 같은 다른 키들을 암호화하는 데 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;extendedKeyUsage : 증서에 포함된 공개 키가 사용될 수 있는 특정 목적을 제한하고 추가적으로 상세화하는 데 사용합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;serverAuth : 서버 인증서에 사용&lt;/li&gt;
&lt;li&gt;clientAuth: 클라이언트 인증서에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;subjectKeyIdentifier : 인증서에 포함된 공개키 식별자 제공 방식&lt;/li&gt;
&lt;li&gt;authorityKeyIdentifier : 인증서가 발행된 인증 기관(CA)의 식별 정보를 포함합니다. 이 정보는 인증서 체인을 검증하는 과정에서 해당 인증서가 어떤 CA에 의해 서명되었는지를 식별하는 데 사용됩니다.&lt;/li&gt;
&lt;li&gt;subjectAltName (Subject Alternative Name)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지정된 도메인 이름들은 해당 SSL/TLS 인증서를 사용하는 서버가 클라이언트로부터 받는 요청의 유효성을 검증하는 데 사용됩니다. 클라이언트는 SSL/TLS 핸드셰이크 과정에서 서버로부터 인증서를 받고, 인증서에 포함된 도메인 이름이 요청한 도메인과 일치하는지 검사합니다.&lt;/li&gt;
&lt;li&gt;과거에는 CN(common name)이 주로 사용되었지만 CN에는 오직 하나의 도메인 이름만 명시할 수 있는 한계가 있어 현재는 CN보다 subjectAltName을 우선적으로 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CA 인증서 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# CA 키 &amp;amp; 인증서 생성
openssl req -x509 -nodes -days 36500 -newkey rsa:2048 -keyout ca.key -out ca.crt -extensions v3_ca -config ssl.conf

# 인증서 점검
openssl x509 -in ca.crt -text -noout&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-x509 : 인증요청서(csr)를 대신 자체 서명된 인증서 생성&lt;/li&gt;
&lt;li&gt;-nodes : 개인 키를 암호 없이 저장&lt;/li&gt;
&lt;li&gt;-days : 인증서 유효기간&lt;/li&gt;
&lt;li&gt;-newkey rsa:2048 : 2048 비트 rsa 키 생성&lt;/li&gt;
&lt;li&gt;-keyout : 생성된 개인키 파일명 지정&lt;/li&gt;
&lt;li&gt;-out : 생성된 자체 서명된 인증서 파일명 지정&lt;/li&gt;
&lt;li&gt;-extensions : 확장 기능 설정으로 config 파일에 명시한 v3_ca를 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Server 인증서 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 서버키 생성
openssl genrsa -out _server.key 2048

# 형식 변환
# genrsa를 사용하여 키를 생성하면 PKCS#1형식으로 생성됩니다. 
# 자바에서는 PKCS#8 형식을 지원하기 때문에 변환해줘야 합니다.
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in _server.key -out server.key

# 키 제거
rm _server.key

# 서버 csr 생성
openssl req -new -key server.key -out server.csr -config ssl.conf

# CA로 서명된 서버 인증서 생성
openssl x509 -req -days 36500 -in server.csr -CA ca.crt -CAkey ca.key -out server.crt -CAcreateserial -extensions v3_req -extfile ssl.conf

# 인증서 점검
openssl x509 -in server.crt -text -noout&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Client 인증서 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;# 클라이언트키 생성
openssl genrsa -out _client.key 2048

# 형식 변환
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in _client.key -out client.key

# 키 제거
rm _client.key

# 클라이언트 csr 생성
openssl req -new -key client.key -out client.csr -config ssl.conf

# CA로 서명된 클라이언트 인증서 생성
openssl x509 -req -days 36500 -in client.csr -CA ca.crt -CAkey ca.key -out client.crt -CAcreateserial -extensions v3_req -extfile ssl.conf 

# 인증서 점검
openssl x509 -in client.crt -text -noout&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트에 mTLS 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;grpc-server&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class ArmeriaConfig {

  // 생략 

  @Bean
  fun armeriaServerConfigurator(
    grpcService: GrpcService,
  ): ArmeriaServerConfigurator {
    return ArmeriaServerConfigurator {

      // 생략  

      /**
       * mTLS 적용
       * https://github.com/grpc/grpc-java/blob/master/examples/example-tls/src/main/java/io/grpc/examples/helloworldtls/HelloWorldServerTls.java
       */
      val serverCertInputStream = GrpcApplication::class.java.classLoader.getResourceAsStream(&quot;tls/server.crt&quot;)!!
      val serverKeyInputStream = GrpcApplication::class.java.classLoader.getResourceAsStream(&quot;tls/server.key&quot;)!!
      val caCertInputStream = GrpcApplication::class.java.classLoader.getResourceAsStream(&quot;tls/ca.crt&quot;)!!

      it.https(8443)
      serverCertInputStream.use { serverCertStream -&amp;gt;
        serverKeyInputStream.use { serverKeyStream -&amp;gt;
          it.tls(serverCertStream, serverKeyStream)
        }
      }
      it.tlsCustomizer { builder -&amp;gt;
        caCertInputStream.use { caCertStream -&amp;gt;
          val caCert = CertificateFactory.getInstance(&quot;X.509&quot;).generateCertificate(caCertStream) as X509Certificate
          builder.trustManager(caCert)
        }
        builder.clientAuth(ClientAuth.REQUIRE)
      }
    }
  }
}&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;실제 운영환경에서는 k8s의 secret으로 마운트 해서 사용하지만, demo 프로젝트이므로 resource 폴더 하위에 인증서를 넣어주고 ArmeriaServerConfigurator 설정에서 인증서 정보를 등록해 줍니다. trustManager에 caCert를 등록했으므로 grpc 서버는 ca 인증서로 서명된 인증서를 가진 client의 요청만 받을 수 있게 됩니다.&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;rest-client&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class GrpcChannelConfig(
  private val grpcTracing: GrpcTracing,
  private val grpcProperties: GrpcProperties,
) {

  @Bean
  fun grpcChannel(): ManagedChannel {
    val clientCertInputStream = RestClientApplication::class.java.classLoader.getResourceAsStream(&quot;tls/client.crt&quot;)!!
    val clientKeyInputStream = RestClientApplication::class.java.classLoader.getResourceAsStream(&quot;tls/client.key&quot;)!!

    val builder = NettyChannelBuilder.forAddress(grpcProperties.endpoint, grpcProperties.port)
      .intercept(grpcTracing.newClientInterceptor())
      .negotiationType(NegotiationType.TLS)

    clientCertInputStream.use { clientCertStream -&amp;gt;
      clientKeyInputStream.use { clientKeyStream -&amp;gt;
        builder.sslContext(
          GrpcSslContexts.forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE) // 서버 인증서 검증 비활성화
            .keyManager(
              clientCertStream, clientKeyStream,
            )
            .build(),
        )
          .build()
      }
    }

    return builder.build()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client 모듈에서는 channel을 생성할 때 인증서 정보를 등록해줍니다. 기본적인 설정에서는 서버의 인증서가 신뢰할 수 있는 CA에 의해 발행되었는지 등을 검증하는 설정이 포함되어 있습니다. 현재 서버 인증서는 자체 서명된 ca에 의해 서명되었기 때문에 검증하지 않도록 trustManager 체이닝에서 검증을 비활성화하는 설정을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 세팅을 마치면 grpc-server와 client는 mTLS 통신을 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cloudflare.com/ko-kr/learning/access-management/what-is-mutual-tls/&quot;&gt;상호 TLS(mTLS)란&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lesstif.com/system-admin/openssl-root-ca-ssl-6979614.html&quot;&gt;OpenSSL 로 ROOT CA 생성 및 SSL 인증서 발급&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Armeria</category>
      <category>armeria</category>
      <category>grpc</category>
      <category>kotlin</category>
      <category>mtls</category>
      <category>TLS</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/85</guid>
      <comments>https://backtony.tistory.com/85#entry85comment</comments>
      <pubDate>Thu, 2 May 2024 21:37:57 +0900</pubDate>
    </item>
    <item>
      <title>Armeria를 활용한 gRPC-kotlin 서버 구축하기</title>
      <link>https://backtony.tistory.com/84</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는 &lt;a href=&quot;https://backtony.tistory.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/armeria%20grpc%20graphql/armeria&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RPC (Remote Procedure Call)와 REST&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RPC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC는 네트워크로 연결된 서버 상의 프로시저(함수, 메서드 등)를 원격으로 호출할 수 있는 기능입니다. 코드 상으로는 마치 로컬 함수의 호출과 같지만 실제로는 함수가 원격 서버에서 실행됩니다. 네트워크 통신을 위한 작업 하나하나 챙기기 귀찮으니 통신이나 call 방식에 신경 쓰지 않고 원격지의 자원을 내 것처럼 사용할 수 있다는 의미입니다. IDL(Interface Definication Language) 기반으로 다양한 언어를 가진 환경에서도 쉽게 확장이 가능하며, 인터페이스 협업에도 용이하다는 장점이 있습니다.&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;지원 언어 : C++, Java, Python, Ruby, Node.js, C#, Go, PHP, Objective-C ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC의 핵심 개념은 'Stub(스텁)'이라는 것입니다. 서버와 클라이언트는 서로 다른 주소 공간을 사용하므로, 함수 호출에 사용된 매개 변수를 꼭 변환해줘야 합니다. 변환하지 않는다면 메모리 매개 변수에 대한 포인터가 다른 데이터를 가리키게 되기 때문입니다. 이 변환을 담당하는 게 스텁입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client stub은 함수 호출에 사용된 파라미터의 변환(Marshalling, 마샬링) 및 함수 실행 후 서버에서 전달된 결과의 변환을, server stub은 클라이언트가 전달한 매개 변수의 역변환(Unmarshalling, 언마샬링) 및 함수 실행 결과 변환을 담당하게 됩니다. 이런 Stub을 이용한 기본적인 RPC 통신 과정은 다음과 같습니다.&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;727&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lFcne/btsGB2v99QC/VkOQ0RzIwOmJL1CcLs7dOk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lFcne/btsGB2v99QC/VkOQ0RzIwOmJL1CcLs7dOk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lFcne/btsGB2v99QC/VkOQ0RzIwOmJL1CcLs7dOk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlFcne%2FbtsGB2v99QC%2FVkOQ0RzIwOmJL1CcLs7dOk%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;727&quot; height=&quot;290&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;290&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;IDL(Interface Definition Language)을 사용하여 호출 규약 정의합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수명, 인자, 반환값에 대한 데이터형이 정의된 IDL 파일을 rpcgen으로 컴파일하면 stub code가 자동으로 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stub Code에 명시된 함수는 원시코드의 형태로, 상세 기능은 server에서 구현됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만들어진 stub 코드는 클라이언트/서버에 함께 빌드합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;client에서 stub에 정의된 함수를 사용할 때, client stub은 RPC runtime을 통해 함수 호출하고 server는 수신된 procedure 호출에 대한 처리 후 결과 값을 반환합니다.&lt;/li&gt;
&lt;li&gt;최종적으로 Client는 Server의 결과 값을 반환받고, 함수를 Local에 있는 것처럼 사용할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 HTTP/1.1 기반으로 URI를 통해 모든 자원(Resource)을 명시하고 HTTP Method를 통해 처리하는 아키텍처입니다. 자원 그 자체를 표현하기에 직관적이고, HTTP를 그대로 계승하였기에 별도 작업 없이도 쉽게 사용할 수 있다는 장점으로 현대에 매우 보편화되어 있지만 REST에도 한계는 존재합니다. REST는 일종의 스타일이지 표준이 아니기 때문에 parameter와 응답 값이 명시적이지 않습니다. 또한 HTTP 메서드의 형태가 제한적이기 때문에 세부 기능 구현에는 제약이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 데이터 전달 format으로 xml, json을 많이 사용합니다. XML은 html과 같이 tag 기반이지만 미리 정의된 태그가 없어(no pre-defined tags) 높은 확장성을 인정받아 이기종간 데이터 전송의 표준이었으나, 다소 복잡하고 비효율적인 데이터 구조 탓에 속도가 느리다는 단점이 있었습니다. 이런 효율 문제를 JSON이 간결한 Key-Value 구조 기반으로 해결하는 듯하였으나, 제공되는 자료형의 한계로 파싱 후 추가 형변환이 필요한 경우가 많아졌습니다. 또한 두 타입 모두 string 기반이라 사람이 읽기 편하다는 장점이 있으나, 바꿔 말하면 데이터 전송 및 처리를 위해선 별도의 Serialization이 필요하다는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gRPC&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 google 사에서 개발한 오픈소스 RPC(Remote Procedure Call) 프레임워크입니다. 이전까지는 RPC 기능은 지원하지 않고, 메세지(JSON 등)를 Serialize 할 수 있는 프레임워크인 PB(Protocol Buffer, 프로토콜 버퍼)만을 제공해 왔는데, google에서 PB 기반 Serizlaizer에 HTTP/2를 결합한 새로운 RPC 프레임워크 탄생시켰습니다.&lt;/p&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;HTTP/2&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;Streaming&lt;/li&gt;
&lt;li&gt;Header Compression&lt;/li&gt;
&lt;li&gt;Multiplexing
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1.x의 경우 플레인 텍스트에 헤더와 바디 등의 데이터를 한 번에 전송했지만, 2.0부터는 헤더와 데이터를 프레임이라는 단위로 분리하고 다른 스트림에 속하는 각각의 프레임들을 프레임 단위로 하나의 커넥션에 상호 배치하여 목적지에 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http/1.1은 기본적으로 클라이언트의 요청이 올 때만 서버가 응답을 하는 구조로 매 요청마다 connection을 생성해야만 합니다. cookie 등 많은 메타 정보들을 저장하는 무거운 header가 요청마다 중복 전달되어 비효율적이고 속도도 느려집니다. http/2에서는 한 connection으로 동시에 여러 개 메시지를 주고받으며, header를 압축하여 중복 제거 후 전달하기에 1.x에 비해 효율적입니다. 또한, 필요시 클라이언트 요청 없이도 서버가 리소스를 전달할 수도 있기 때문에 클라이언트 요청을 최소화할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ProtoBuf (Protocol Buffer, 프로토콜 버퍼)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protocol Buffer는 google 사에서 개발한 구조화된 데이터를 직렬화(Serialization)하는 기법입니다.&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;773&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czOKQW/btsGCuseeeR/WtRJTUBUdvjAnSk69KgDX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czOKQW/btsGCuseeeR/WtRJTUBUdvjAnSk69KgDX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czOKQW/btsGCuseeeR/WtRJTUBUdvjAnSk69KgDX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczOKQW%2FbtsGCuseeeR%2FWtRJTUBUdvjAnSk69KgDX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;355&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;355&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;직렬화란, 데이터 표현을 바이트 단위로 변환하는 작업을 의미합니다. 위 그림처럼 같은 정보를 저장해도 text 기반인 json인 경우 82 byte가 소요되는데 반해, 직렬화된 protocol buffer는 필드 번호, 필드 유형 등을 1byte로 받아서 식별하고, 주어진 length 만큼만 읽도록 하여 단 33 byte만 필요하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Proto File&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proto File에 Protocol Buffer의 기본 정보를 명세하여 메시지를 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;타입 정의&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;height: 172px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;th style=&quot;height: 19px;&quot;&gt;Java Type&lt;/th&gt;
&lt;th style=&quot;height: 19px;&quot;&gt;Proto Type&lt;/th&gt;
&lt;th style=&quot;height: 19px;&quot;&gt;default value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;int&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;int32&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;long&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;int64&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;float&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;float&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;double&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;double&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;boolean&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;bool&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;string&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;string&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;empty string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;byte[]&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;bytes&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;empty bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;collection / List&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;repeated&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;empty list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;map&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;map&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;empty map&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proto Type의 경우, 기본적으로 null값을 허용하지 않습니다. null값을 허용하기 위해서는 별도의 google에서 제공하는 wrapper 타입을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 정의&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;protobuf&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

option java_multiple_files = true;
option java_package = &quot;com.example.demo.proto.ecommerce.product&quot;;

import &quot;google/protobuf/timestamp.proto&quot;;
import &quot;google/protobuf/empty.proto&quot;;
import &quot;google/protobuf/wrappers.proto&quot;;

package ecommerce.product;

service ProductInfo {
    rpc getProduct(ProductID) returns (Product);
}

message Product {
    string id = 1;
    string name = 2;
    google.protobuf.StringValue description = 3;
    double price = 4;
    google.protobuf.Timestamp registeredDate = 6;
}

message ProductID {
    string value = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;syntax
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;규약을 명시하는 부분으로 proto version 3의 규약을 따르겠다고 명시합니다.&lt;/li&gt;
&lt;li&gt;proto2와 proto3는 지원하는 언어에 차이가 있으며 문법적으로도 차이가 있습니다.&lt;/li&gt;
&lt;li&gt;proto2의 경우에는 optional, required를 사용했지만 proto3에서는 deprecated 되었고 repeated만 proto3에서 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;java_multiple_files
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 옵션 값은 false&lt;/li&gt;
&lt;li&gt;false로 지정할 경우 오직 하나의 .java 파일이 생성되고, top-level 메시지, 서비스, enum에 대해 생성된 모든 자바 클래스(enum 등)는 outer 클래스 내에 중첩됩니다.&lt;/li&gt;
&lt;li&gt;true로 지정할 경우 위와 같은 상황에 대해 각각의 .java 파일이 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;import
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;proto Type에서는 기본적으로 null 값을 허용하지 않지만, google에서 제공하는 wrapper 타입을 사용하면 null을 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;값을 명시하지 않으면 기본적으로 default value가 지정되지만 값을 꺼내서 사용할 때, XXOrNull 혹은 hasXX 함수 등을 사용하여 값이 채워지지 않았는지 확인할 수 있는 방법이 제공됩니다.&lt;/li&gt;
&lt;li&gt;Timestamp 타입으로 시간 관련한 필드를 정의할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;java_package
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 package 경로로 생성되나 명시하여 생성되는 파일들의 package 경로를 지정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;package
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;message type 간의 이름이 겹치는 경우, 구분할 때 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;service
&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;client는 stub을 사용하여 해당 인터페이스를 호출하고 server에서는 해당 인터페이스를 구현하게 됩니다.&lt;/li&gt;
&lt;li&gt;server가 해당 인터페이스를 구현하게 되는데 spring mvc 관점에서 본다면 controller와 유사합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;message
&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;메시지에 정의된 필드들은 각각 고유한 번호(Field Tag)를 갖게 되고 encoding 이후 binary data에서 필드를 식별하는 데 사용됩니다.&lt;/li&gt;
&lt;li&gt;최소 1부터 536,870,911까지 지정 가능하며, 19000 ~ 19999는 프로토콜 버퍼 구현을 위해 reserved 된 값이므로 사용할 수 없습니다.&lt;/li&gt;
&lt;li&gt;필드 번호가 1 ~ 15일 때는 1byte, 16 ~ 2047은 2byte를 Tag로 가져가게 되기 때문에 자주 호출되는 필드에 대해서는 1 ~ 15로 지정하는 것이 권장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버전 호환성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;message Product {
  string id = 1;
  string name = 2;
  google.protobuf.StringValue description = 3;
  double price = 4;
  google.protobuf.Timestamp registeredDate = 6 [deprecated=true]; // 필드 제거
  // reserved = 6;
  google.protobuf.Timestamp createdAt = 7; // 새로운 필드 추가
}&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;필드번호는 메시지의 호환성을 유지하는 핵심요소입니다. 제거할 때는 해당 번호를 재사용하지 않도록 주의해야 합니다. 배포 이후에 protobuf에서 필드 제거가 필요한 경우, deprecated를 명시하는 방법과 필드 자체를 제거하는 방법이 있습니다. 해당 필드를 제거하는 경우 reserved 키워드를 사용하여 재사용을 방지할 수 있습니다. 필드를 추가할 경우에는 새로운 번호를 사용해야 합니다. 자세한 내용은 아래 문서를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://protobuf.dev/programming-guides/proto3/#reserved&quot;&gt;https://protobuf.dev/programming-guides/proto3/#reserved&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://stackoverflow.com/questions/60490487/whats-the-best-way-to-deprecate-a-field-in-protocol-buffer-v3-reserved-vs-depre&quot;&gt;https://stackoverflow.com/questions/60490487/whats-the-best-way-to-deprecate-a-field-in-protocol-buffer-v3-reserved-vs-depre&lt;/a&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Armeria&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Armeria는 Line에서 개발한 MSA 프레임워크입니다. 하나의 포트에서 여러 가지 프로토콜(http, gRPC, Thrift)을 사용할 수 있고 gRPC의 문서화를 자동으로 생성해 주는 등 다양한 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Armeria의 소개 및 장점은 아래 링크를 참고 바랍니다.&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;&lt;a href=&quot;https://armeria.dev/&quot;&gt;공식 홈페이지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/introduce-armeria&quot;&gt;Armeria 소개 Line Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/hello-armeria-bye-spring&quot;&gt;LINE 개발자들이 Spring 대신 Armeria를 사용하는 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/6080222&quot;&gt;Spring WebFlux와 Armeria를 이용하여 Microservice에 필요한 Reactive + RPC 동시에 잡기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티모듈 프로젝트 구축하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티모듈 형태로 Armeria &amp;amp; spring를 사용한 grpc-server와 일반적인 spring webflux을 사용한 grpc-client 서버를 구축해 보겠습니다. 모듈은 다음과 같습니다.&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;protos : proto file 정의하는 모듈&lt;/li&gt;
&lt;li&gt;stub : proto file을 rpcgen으로 컴파일하여 stub code를 생성하는 모듈&lt;/li&gt;
&lt;li&gt;grpc : Armeria &amp;amp; Spring 을 사용한 grpc server 모듈&lt;/li&gt;
&lt;li&gt;rest-client : grpc-server를 호출하는 spring client 모듈&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드는 member와 team 모듈을 사용하나 포스팅을 간소화하기 위해 member 모듈만 설명하겠습니다. 전체 코드는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/armeria%20grpc%20graphql/armeria&quot;&gt;여기&lt;/a&gt;를 확인 바랍니다. 예시 코드는 &lt;a href=&quot;https://github.com/grpc/grpc-kotlin/tree/master/examples&quot;&gt;grpc-kotlin-example&lt;/a&gt;와 &lt;a href=&quot;https://github.com/line/armeria-examples&quot;&gt;Armeria-example&lt;/a&gt; 를 참고하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;protos 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;└── protos
  ├── build.gradle.kts
  └── src
      └── main
          └── proto
              ├── member
              │ └── member.proto
              └── team
                  └── team.proto&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;protos 모듈의 트리구조는 위와 같습니다.&lt;/p&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;member.proto 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;protobuf&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

option java_multiple_files = true;
option java_package = &quot;com.example.proto.member&quot;;

import &quot;google/protobuf/timestamp.proto&quot;;
import &quot;google/protobuf/empty.proto&quot;;
import &quot;google/protobuf/wrappers.proto&quot;;

package member;

service MemberHandler {
    rpc createMember(CreateMemberRequest) returns (MemberResponse);
    rpc getMembersByTeamId(TeamId) returns (MemberListResponse);
}

message CreateMemberRequest {
    string name = 1;
    google.protobuf.StringValue introduction = 2;
    Country country = 3;
    Type type = 4;
    google.protobuf.Int64Value teamId = 5;
    string requestedBy = 6;
}

enum Type {
    UNKNOWN_TYPE = 0;
    INDIVIDUAL = 1;
    COMPANY = 2;
}

enum Country {
    UNKNOWN_COUNTRY = 0;
    KR = 1;
    US = 2;
    JP = 3;
}

message MemberResponse {
    int64 id = 1;
    string name = 2;
    google.protobuf.StringValue introduction = 3;
    Type type = 4;
    Country country = 5;
    google.protobuf.Int64Value teamId = 6;
    string registeredBy = 7;
    google.protobuf.Timestamp registeredDate = 8;
    string modifiedBy = 9;
    google.protobuf.Timestamp modifiedDate = 10;
}

message TeamId {
    int64 id = 1;
}

message MemberListResponse {
    repeated MemberResponse member = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member를 생성하고 teamId로 member를 조회하는 rpc service를 정의했습니다. enum의 경우 필드 고유 번호가 0이 존재하는데, 아무런 값이 들어오지 않으면 0에 해당하는 enum 값이 default로 설정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;repeated 타입의 필드의 경우 변수명을 members(복수)로 지정하게 되면 code gen 된 클래스에서 필드명이 membersList로 지정됩니다. 이러한 이슈 때문에 repeated의 경우 단수로 네이밍하고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import org.springframework.boot.gradle.tasks.bundling.BootJar

tasks.getByName&amp;lt;BootJar&amp;gt;(&quot;bootJar&quot;) {
    enabled = false
}

tasks.getByName&amp;lt;Jar&amp;gt;(&quot;jar&quot;) {
    enabled = true
}

java {
    sourceSets.getByName(&quot;main&quot;).resources.srcDir(&quot;src/main/proto&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src/main/proto 디렉토리를 main 소스셋의 리소스 디렉토리로 추가합니다. stub모듈에서 해당 모듈을 가져다가 사용할 예정입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;stub 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;└── stub
    └──  build.gradle.kts&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;stub 모듈의 트리구조는 위와 같습니다.&lt;/p&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;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import com.google.protobuf.gradle.id
import com.google.protobuf.gradle.protobuf
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
    id(&quot;com.google.protobuf&quot;) version &quot;0.9.4&quot;
}

val grpcKotlinVersion = &quot;1.4.1&quot;
val grpcProtoVersion = &quot;1.63.0&quot;
val grpcVersion = &quot;3.25.3&quot;

tasks.getByName&amp;lt;BootJar&amp;gt;(&quot;bootJar&quot;) {
    enabled = false
}

tasks.getByName&amp;lt;Jar&amp;gt;(&quot;jar&quot;) {
    enabled = true
}

// https://github.com/grpc/grpc-kotlin/blob/master/examples/stub/build.gradle.kts
dependencies {
    protobuf(project(&quot;:protos&quot;))

    api(&quot;io.grpc:grpc-stub:$grpcProtoVersion&quot;)
    api(&quot;io.grpc:grpc-protobuf:$grpcProtoVersion&quot;)
    api(&quot;io.grpc:grpc-kotlin-stub:$grpcKotlinVersion&quot;) // kotlin stub 제공
    api(&quot;com.google.protobuf:protobuf-kotlin:$grpcVersion&quot;) // kotlin 코드 생성 도구
    api(&quot;io.grpc:grpc-netty:$grpcProtoVersion&quot;) // stub NettyChannel에 사용
}

protobuf {
    // Configure the protoc executable.
    protoc {
        // Download from the repository.
        artifact = &quot;com.google.protobuf:protoc:$grpcVersion&quot;
    }

    // Locate the codegen plugins.
    plugins {
        // Locate a plugin with name 'grpc'.
        id(&quot;grpc&quot;) {
            // Download from the repository.
            artifact = &quot;io.grpc:protoc-gen-grpc-java:$grpcProtoVersion&quot;
        }
        // Locate a plugin with name 'grpcKt'.
        id(&quot;grpckt&quot;) {
            // Download from the repository.
            artifact = &quot;io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion:jdk8@jar&quot;
        }
    }

    // generate code
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id(&quot;grpc&quot;)
                id(&quot;grpckt&quot;)
            }
            it.builtins {
                id(&quot;kotlin&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stub 모듈은 protos 모듈의 proto file을 가져와 proto file 정의에 따른 java, kotlin stub 코드들을 만들어내는 모듈입니다. 해당 세팅에서 build를 하면 build/generated/source/proto/main 경로에 proto file에 정의된 형태의 코드들이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;grpc 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grpc 모듈은 Armeria를 사용하여 grpc 서버를 구축합니다. grpc 모듈의 자세한 구조는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/armeria%20grpc%20graphql/armeria&quot;&gt;여기&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle.kts&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;val armeriaVersion = &quot;1.27.0&quot;

dependencies {
    implementation(project(&quot;:stub&quot;))

    // armeria
    // https://github.com/line/armeria-examples/blob/main/grpc/build.gradle
    implementation(platform(&quot;io.netty:netty-bom:4.1.106.Final&quot;))
    implementation(platform(&quot;com.linecorp.armeria:armeria-bom:$armeriaVersion&quot;))
    implementation(&quot;com.linecorp.armeria:armeria-kotlin:$armeriaVersion&quot;)
    implementation(&quot;com.linecorp.armeria:armeria-spring-boot3-starter:$armeriaVersion&quot;)
    implementation(&quot;com.linecorp.armeria:armeria-spring-boot3-actuator-starter:$armeriaVersion&quot;)

    // grpc
    implementation(&quot;com.linecorp.armeria:armeria-grpc:$armeriaVersion&quot;)

    // r2dbc
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-r2dbc&quot;)
    implementation(&quot;io.asyncer:r2dbc-mysql:1.1.0&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;proto file을 사용하여 code gen을 해주는 stub 프로젝트를 추가하고 armeria와 grpc를 위한 의존성을 추가해 줍니다. DI를 사용하기 위해 spring 의존성과 데모 코드에서는 db를 r2dbc-mysql을 사용하므로 r2dbc 관련 의존성도 추가해 줍니다.&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;MemberHandler&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GrpcHandler
class MemberHandler(
    private val memberService: MemberService,
) : MemberHandlerGrpcKt.MemberHandlerCoroutineImplBase() {

    override suspend fun createMember(request: CreateMemberRequest): MemberResponse {
        return memberService.createMember(MemberMapper.generateCreateMemberRequest(request))
            .let { MemberMapper.generateMemberResponse(it) }
    }

    // ... 생략
}&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;앞서 member.proto 파일에서 정의했던 &lt;b&gt;&lt;code&gt;service MemberHandler&lt;/code&gt;&lt;/b&gt;가 stub 모듈에서 code gen 되면서 MemberHandlerGrpcKt 와 같은 클래스들이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;MemberHandlerGrpcKt.MemberHandlerCoroutineImplBase()&lt;/code&gt;&lt;/b&gt; 추상 클래스의 메서드를 재정의함으로써 grpc service의 구현이 시작됩니다. spring mvc 관점에서 보면 Controller에 해당한다고 볼 수 있습니다. 함수의 인자로 사용되는 request와 response 모두 proto file에 정의해 두었던 message가 stub 모듈에서 code gen 되면서 생성된 클래스들입니다. @GrpcHandler 애노테이션은 단순 마킹용 애노테이션으로 grpc의 구현체라는 것을 명시하기 위해서 달아두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component
annotation class GrpcHandler&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MemberMapper와 ProtoTypeUtil&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;roboconf&quot;&gt;&lt;code&gt;object MemberMapper {

    fun generateCreateMemberRequest(request: CreateMemberRequest): MemberDto.CreateMemberRequest {

        return with(request) {
            MemberDto.CreateMemberRequest(
                name = name,
                introduction = introductionOrNull?.value, // google.protobuf
                type = Member.Type.valueOf(type.name),
                country = Member.Country.valueOf(country.name),
                teamId = teamIdOrNull?.value, // google.protobuf
                requestedBy = requestedBy,
            )
        }
    }

    fun generateMemberResponse(member: Member): MemberResponse {

        return memberResponse {
            id = member.id!!
            name = member.name
            member.introduction?.let {
                introduction = StringValue.of(it) // google.protobuf
            }
            type = Type.valueOf(member.type.name)
            country = Country.valueOf(member.country.name)
            member.teamId?.let {
                teamId = Int64Value.of(it) // google.protobuf
            }
            registeredBy = member.registeredBy
            registeredDate = member.registeredDate.toTimestamp() // google.protobuf
            modifiedBy = member.modifiedBy
            modifiedDate = member.modifiedDate.toTimestamp() // google.protobuf
        }
    }

    fun generateMemberListResponse(members: List&amp;lt;Member&amp;gt;): MemberListResponse {
        return memberListResponse {
            member.addAll(members.map { generateMemberResponse(it) }) // repeated field
        }
    }
}&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;MemberMapper는 code gen 된 클래스와 내부 Dto로 혹은 도메인으로 변환하는 매퍼클래스입니다. code gen으로 생성된 클래스의 경우 코틀린 dsl이 제공되므로 이를 사용하여 더 간결하게 구현할 수 있습니다. protobuf를 정의할 때, google.protobuf를 사용한 경우 XXXorNull 메서드를 사용하여 값이 들어오지 않은 경우 null값을 꺼낼 수 있으며 반대로 google.protobuf 타입으로 만들기 위해서는 XXValue.of 메서드를 사용할 수 있습니다. 그리고 repeated 타입의 필드의 경우 addAll 메서드를 사용하여 값을 넣어줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;protobuf에는 시간 관련된 타입으로 TimeStamp만을 제공하기 때문에 LocalDateTime을 TimeStamp 타입으로 변환하기 위해서 아래와 같이 확장함수를 정의하여 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// ProtoTypeUtil
fun LocalDateTime.toTimestamp(): Timestamp {
  return toTimestamp(ZoneId.systemDefault())
}

fun LocalDateTime.toTimestamp(zoneId: ZoneId): Timestamp {
  val instant = this.atZone(zoneId).toInstant()
  return Timestamp.newBuilder()
    .setNanos(instant.nano)
    .setSeconds(instant.epochSecond)
    .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Interceptor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grpc interceptor는 크게 serverInterceptor와 clientInterceptor로 구분되며 각 구분의 하위로 streaming과 unary로 다시 분류됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;@ThreadSafe
public interface ServerInterceptor {

  &amp;lt;ReqT, RespT&amp;gt; ServerCall.Listener&amp;lt;ReqT&amp;gt; interceptCall(
      ServerCall&amp;lt;ReqT, RespT&amp;gt; call,
      Metadata headers,
      ServerCallHandler&amp;lt;ReqT, RespT&amp;gt; next);
}&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;ServerInterceptor 인터페이스는 interceptCall 단일 메서드만 가지고 있습니다. 이 메서드는 클라이언트로부터의 각 호출에 대해 실행되며, 인터셉터 체인을 통해 다음 인터셉터 또는 실제 서비스 메서드로 요청을 전달합니다. ServerCall은 클라이언트로부터 받은 RPC(원격 프로시저 호출) 요청을 나타냅니다. 이 객체를 통해 서버는 클라이언트에게 응답을 보낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface ServerCallHandler&amp;lt;RequestT, ResponseT&amp;gt; {

  ServerCall.Listener&amp;lt;RequestT&amp;gt; startCall(
      ServerCall&amp;lt;RequestT, ResponseT&amp;gt; call,
      Metadata headers);
}&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;ServerCallHandler는 serverCall과 Metadata를 인자로 받아서 요청 처리를 준비하는 작업을 수행하고, 이후 요청 처리 주체인 serverCall.Listener를 반환하는 인터페이스 입니다. ServerCall.Listener는 클라이언트로부터 추가적인 메시지를 수신하거나, 요청이 반쪽 닫힘 상태(half-closed)로 전환되거나 요청이 완료되었을 때 등 다양한 이벤트를 처리하는 콜백 메서드를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class TestInterceptor : ServerInterceptor {

    private val log = KotlinLogging.logger {  }

    override fun &amp;lt;ReqT : Any?, RespT : Any?&amp;gt; interceptCall(
        call: ServerCall&amp;lt;ReqT, RespT&amp;gt;,
        headers: Metadata,
        next: ServerCallHandler&amp;lt;ReqT, RespT&amp;gt;,
    ): Listener&amp;lt;ReqT&amp;gt; {

        // 전처리
        log.info(&quot;pre handle&quot;)

        return next.startCall(call, headers)
    }
}&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;인터셉터에서 전처리는 serverCallHandler의 startCall 메서드를 호출하기 전에 수행할 수 있습니다. 후처리는 두 가지 방법으로 수행할 수 있습니다.&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;serverCall 재정의&lt;/li&gt;
&lt;li&gt;listener 재정의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleForwardingServerCall과 SimpleForwardingServerCallListener는 각각 ServerCall과 ServerCall.Listener의 편리한 래퍼 클래스로, 이 래퍼들은 gRPC 서버에서 요청을 다루는 데 필요한 메서드를 상속받아, 개발자가 특정 메소드를 오버라이드하는 것을 간소화합니다. 이를 활용하여 아래와 같이 간단한 로깅 인터셉터를 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class SimpleLoggingInterceptor : ServerInterceptor {

  override fun &amp;lt;ReqT : Any?, RespT : Any?&amp;gt; interceptCall(
    call: ServerCall&amp;lt;ReqT, RespT&amp;gt;,
    headers: Metadata,
    next: ServerCallHandler&amp;lt;ReqT, RespT&amp;gt;,
  ): Listener&amp;lt;ReqT&amp;gt; {
    val serverCall = LoggingServerCall(
      delegate = call,
      startCallMillis = System.currentTimeMillis(),
    )

    return LoggingServerCallListener(next.startCall(serverCall, headers))
  }

  class LoggingServerCall&amp;lt;ReqT, RespT&amp;gt;(
    private val delegate: ServerCall&amp;lt;ReqT, RespT&amp;gt;,
    private val startCallMillis: Long,
  ) : ForwardingServerCall.SimpleForwardingServerCall&amp;lt;ReqT, RespT&amp;gt;(delegate) {

    override fun close(status: Status, trailers: Metadata?) {
      log.info {
        &quot;status:${status.code.name} &quot; +
                &quot;rpc:${delegate.methodDescriptor.fullMethodName.replace(&quot;/&quot;, &quot;.&quot;)} &quot; +
                &quot;responseTime:${(System.currentTimeMillis() - startCallMillis)}ms &quot;
      }
      super.close(status, trailers)
    }
  }

  class LoggingServerCallListener&amp;lt;ReqT&amp;gt;(
    delegate: Listener&amp;lt;ReqT&amp;gt;,
  ) : ForwardingServerCallListener.SimpleForwardingServerCallListener&amp;lt;ReqT&amp;gt;(delegate) {

    override fun onMessage(message: ReqT) {
      log.info(&quot;Receive Message : ${message.toString().trim()}&quot;)
      super.onMessage(message)
    }
  }

  companion object {
    private val log = KotlinLogging.logger { }
  }
}&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;SimpleForwardingServerCall과 SimpleForwardingServerCallListener를 재정의하여 이외의 다양한 지점에서 로직을 구현할 수 있습니다.&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;1100&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPEMya/btsGAZttzph/h0yMHRu02RS2MysjEdPUpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPEMya/btsGAZttzph/h0yMHRu02RS2MysjEdPUpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPEMya/btsGAZttzph/h0yMHRu02RS2MysjEdPUpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPEMya%2FbtsGAZttzph%2Fh0yMHRu02RS2MysjEdPUpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1100&quot; height=&quot;1200&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;1200&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;pre class=&quot;pf&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;호출 순서

1. onReady(ServerCallHandler) : 서버가 클라이언트로부터 데이터를 받을 준비가 되었을 때
2. onMessage(ServerCallHandler) : 클라이언트가 보낸 메시지를 수신
3. onHalfClose(ServerCallHandler) : 클라이언트가 더 이상 데이터를 보내지 않겠다는 신호
  * grpc는 http2 스트리밍 방식으로 동작하기 때문에 client에서 모든 요청을 보냈음을 알리는 신호를 위한 것
  * 따라서 서버는 onHalfClose 이후에 비즈니스 로직을 실행
4. sendHeader(ServerCall) : 응답을 시작하기 전에 응답 헤더를 전송.
5. sendMessage(ServerCall) : 응답 데이터 전송
6. close(ServerCall) : 호출을 종료하고 종료 상태(status)와 메타데이터를 클라이언트에게 전송
7. onCancel(ServerCallHandler) : 클라이언트가 요청을 취소하거나 연결이 끊어졌을 때
7. onComplete(ServerCallHandler) : 서버가 요청을 성공적으로 처리하고 응답을 클라이언트에게 보낸 후 호출&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://grpc.github.io/grpc-java/javadoc/io/grpc/ForwardingClientCall.html&quot;&gt;https://grpc.github.io/grpc-java/javadoc/io/grpc/ForwardingClientCall.html&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://grpc.github.io/grpc-java/javadoc/io/grpc/ClientCall.Listener.html&quot;&gt;https://grpc.github.io/grpc-java/javadoc/io/grpc/ClientCall.Listener.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 위 문서를 참고 바랍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예외 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 @ControllerAdvice로 전체적인 예외처리를 담당했다면 grpc에서는 interceptor를 사용하여 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class GlobalExceptionInterceptor : ServerInterceptor {

    override fun &amp;lt;ReqT : Any?, RespT : Any?&amp;gt; interceptCall(
        call: ServerCall&amp;lt;ReqT, RespT&amp;gt;,
        headers: Metadata,
        next: ServerCallHandler&amp;lt;ReqT, RespT&amp;gt;,
    ): ServerCall.Listener&amp;lt;ReqT&amp;gt; {

        return next.startCall(ExceptionServerCall(call), headers)
    }

    class ExceptionServerCall&amp;lt;ReqT, RespT&amp;gt;(
        delegate: ServerCall&amp;lt;ReqT, RespT&amp;gt;,
    ) : SimpleForwardingServerCall&amp;lt;ReqT, RespT&amp;gt;(delegate) {

        override fun close(status: Status, trailers: Metadata?) {
            if (status.isOk) {
                super.close(status, trailers)
            } else {
                val exceptionStatus: Status = handleException(status.cause)
                log.error(&quot;gRPC exception : \n$exceptionStatus&quot;, status.cause)
                super.close(exceptionStatus, trailers)
            }
        }

        /**
         * Exception을 grpc error Code로 변경
         */
        private fun handleException(e: Throwable?): Status {
            when (e) {
                is IllegalArgumentException -&amp;gt; return Status.INVALID_ARGUMENT.withDescription(e.message)
            }

            return Status.INTERNAL.withDescription(e?.message)
        }
    }

    companion object {
        private val log = KotlinLogging.logger { }
    }
}&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;grpc는 http status code와 달리 별도의 status 코드를 사용합니다. 관련 코드는 &lt;a href=&quot;https://grpc.io/docs/guides/status-codes/&quot;&gt;공식 문서&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Armeria Config&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArmeriaConfig 클래스는 하나씩 쪼개서 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class ArmeriaConfig {

    @Bean
    fun grpcService(
        allServiceBean: List&amp;lt;AbstractCoroutineServerImpl&amp;gt;,
    ): GrpcService {
        val grpcServiceBuilder = GrpcService.builder()
            .enableUnframedRequests(true)
            .intercept(SimpleLoggingInterceptor(), GlobalExceptionInterceptor())

        allServiceBean.forEach {
            logger.info(&quot;Register Grpc Bean : {}&quot;, it.javaClass.name)
            grpcServiceBuilder.addService(it)
        }

        return grpcServiceBuilder.build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 grpc의 진입점에 해당하는 service는 proto file의 service code gen으로 생성된 XX.XXImpleBase 클래스를 상속받아 구현했습니다. &lt;a href=&quot;https://armeria.dev/docs/server-grpc&quot;&gt;공식 문서&lt;/a&gt;에 따르면 각각의 서비스마다 등록하도록 가이드가 되어있지만 서비스가 많아질수록 이는 번거로운 작업이므로 빈으로 등록해 두고 ArmeriaServerConfigurator에 등록해주려고 합니다. XXXImplBase는 AbstractCoroutineServerImpl를 상속받기 때문에 이를 활용하여 인자로 구현체들을 주입받아 grpcServiceBuilder에 모든 서비스를 추가하고, 앞서 만들어주었던 인터셉터도 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface ArmeriaServerConfigurator extends Ordered {
  void configure(ServerBuilder serverBuilder);
}&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;마지막으로 위 인터페이스를 구현하여 armeria 설정을 마무리합니다. serverBuilder에는 다양한 옵션이 있으므로 옵션을 추가하고자 한다면 &lt;a href=&quot;https://javadoc.io/doc/com.linecorp.armeria/armeria-javadoc/latest/com/linecorp/armeria/server/ServerBuilder.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class ArmeriaConfig {

    @Bean
    fun armeriaServerConfigurator(
        grpcService: GrpcService,
    ): ArmeriaServerConfigurator {        

        return ArmeriaServerConfigurator {

            // Max Request Length 증설
            it.maxRequestLength(32 * 1024 * 1024)

            // Grpc 사용을 위한 서비스 등록 
            it.service(grpcService)

            // Docs 생성을 위한 서비스 등록
            // /docs 경로에 대해서 DocService를 등록 
            it.serviceUnder(&quot;/docs&quot;, DocService())

            // https://armeria.dev/docs/server-decorator
            // Logging을 위한 Decorator 등록
            it.decorator(LoggingService.newDecorator())
//            it.decorator(LoggingService.builder()
//                .requestLogLevel(LogLevel.INFO)  // 요청 로그 레벨 설정
//                .successfulResponseLogLevel(LogLevel.INFO)  // 성공 응답 로그 레벨 설정
//                .failureResponseLogLevel(LogLevel.ERROR)  // 실패 응답 로그 레벨 설정
//                .newDecorator()
//            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DocService : swagger처럼 web상에서 grpc 테스트를 손쉽게 할 수 있는 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;Decorator : Armeria에서는 들어오는 요청이나 나가는 응답을 가로채기 위해 다른 서비스를 데코레이팅 서비스 또는 데코레이터를 제공합니다. 이름에서 알 수 있듯이 데코레이터 패턴을 구현한 것입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LoggingService는 로깅을 제공합니다. &lt;code&gt;com.linecorp.armeria.server.logging.LoggingService: DEBUG&lt;/code&gt; 로 로그레벨을 지정해 주면 요청과 응답에 대해 로깅이 되며, 로그레벨을 yml에 지정하지 않고 위의 주석처럼 명시해 줄 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;rest-client 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    implementation(project(&quot;:stub&quot;))

    implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rest-client 모듈은 grpc-server을 호출하는 client 모듈입니다. grpc 호출을 위해 stub을 추가해 줍니다.&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;channel&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class GrpcChannelConfig(
    private val grpcProperties: GrpcProperties,
) {

    @Bean
    fun grpcChannel(): ManagedChannel {
        return NettyChannelBuilder.forAddress(grpcProperties.endpoint, grpcProperties.port)
            .usePlaintext()
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 커넥션, 커넥션풀, 로드밸런싱 등을 추상화하고 있는 channel을 제공합니다. channel은 서버로의 연결을 관리하며 클라이언트가 RPC를 서버에 호출할 때 사용됩니다. ManagedChannel은 연결 설정, 유지 통신 중 오류 처리, 연결 종료와 같은 작업을 자동으로 처리하는 기능을 제공합니다. 그리고 channel은 클라이언트 스텁을 생성할 때 사용됩니다.&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;StubFactory&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client는 grpc-server를 호출하기 위해서 stub을 만들어 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class MemberStub(
    private val stubFactory: StubFactory
) {

    @Bean
    fun memberServiceStub(): MemberHandlerGrpcKt.MemberHandlerCoroutineStub {
        return stubFactory.createStub(MemberHandlerGrpcKt.MemberHandlerCoroutineStub::class)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class StubFactory(
    private val grpcProperties: GrpcProperties,
    private val grpcChannel: ManagedChannel,
) {

    fun &amp;lt;T&amp;gt; createStub(
        stubClass: KClass&amp;lt;T&amp;gt;,
        timeout: Long = grpcProperties.timeout,
    ): T where T : AbstractCoroutineStub&amp;lt;T&amp;gt; {
        val constructor = stubClass.primaryConstructor!!
        return constructor.call(grpcChannel, CallOptions.DEFAULT)
            .withInterceptors(TimeoutInterceptor(timeout))
//            .withDeadlineAfter(3, TimeUnit.SECONDS)      
    }
}&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;하나씩 stub을 만들어서 빈으로 등록해 줘도 무관하나 StubFactory 클래스를 만들어서 stub 생성을 한 곳에서 처리하여 공통화하고 중복을 줄입니다. reflection을 사용해서 만들어주고 timeout을 설정하는 interceptor를 추가한 stub을 생성해서 반환하는 메서드를 제공합니다. 기본적으로 stub은 withDeadlineAfter라는 메서드로 timeout을 지정할 수 있으나 withDeadlineAfter을 사용해서 만들어진 stub 인스턴스는 만들어진 순간부터 timeout 카운트가 진행됩니다. 따라서 withDeadlineAfter 메서드 대신 timeout을 처리하는 interceptor를 추가해 주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class TimeoutInterceptor(
    private val timeout: Long,
) : ClientInterceptor {

    override fun &amp;lt;ReqT : Any?, RespT : Any?&amp;gt; interceptCall(
        method: MethodDescriptor&amp;lt;ReqT, RespT&amp;gt;,
        callOptions: CallOptions,
        next: Channel,
    ): ClientCall&amp;lt;ReqT, RespT&amp;gt; {

        return next.newCall(method, callOptions.withDeadlineAfter(timeout, TimeUnit.MILLISECONDS))
    }
}&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;client interceptor는 serverInterceptor와 크게 차이가 나지 않으며 다음과 같이 동작합니다.&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;696&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzqmc/btsGDzmigwO/ioSXYM1oABnntMAaketsz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzqmc/btsGDzmigwO/ioSXYM1oABnntMAaketsz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzqmc/btsGDzmigwO/ioSXYM1oABnntMAaketsz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flzqmc%2FbtsGDzmigwO%2FioSXYM1oABnntMAaketsz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;800&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MemberClient&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class MemberClient(
    private val memberServiceStub: MemberHandlerGrpcKt.MemberHandlerCoroutineStub,
) {
    suspend fun createMember(request: MemberDto.CreateMemberRequest): MemberDto.MemberResponse {
        return memberServiceStub.createMember(MemberMapper.generateCreateMemberRequest(request))
            .let { MemberMapper.generateMemberResponse(it) }
    }

    suspend fun getMembersByTeamId(teamId: Long): MemberDto.MemberListResponse {
        return memberServiceStub.getMembersByTeamId(MemberMapper.generateTeamId(teamId))
            .let { MemberMapper.generateMemberListResponse(it) }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 호출 시에는 위와 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.naver.com/n_cloudplatform/221751268831&quot;&gt;시대의 흐름, gRPC 깊게 파고들기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineering.kabu.com/entry/2021/03/31/162401&quot;&gt;Introduction to Java gRPC Interceptor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://armeria.dev/docs/server-grpc&quot;&gt;Armeria 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grpc.io/docs/what-is-grpc/&quot;&gt;grpc 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Armeria</category>
      <category>armeria</category>
      <category>grpc</category>
      <category>kotlin</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/84</guid>
      <comments>https://backtony.tistory.com/84#entry84comment</comments>
      <pubDate>Mon, 15 Apr 2024 00:55:03 +0900</pubDate>
    </item>
    <item>
      <title>Spring - CircuitBreaker</title>
      <link>https://backtony.tistory.com/83</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공부한 내용을 정리하는 &lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는 &lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/spring/circuit/circuit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CircuitBreaker란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;1518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWpS9M/btsGshlOjlK/gTaWp22NqktnpQZdlCBIF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWpS9M/btsGshlOjlK/gTaWp22NqktnpQZdlCBIF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWpS9M/btsGshlOjlK/gTaWp22NqktnpQZdlCBIF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWpS9M%2FbtsGshlOjlK%2FgTaWp22NqktnpQZdlCBIF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;547&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;1518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;CircuitBreaker는 문제가 발생한 지점을 감지하고 실패하는 요청을 계속하지 않도록 방지하며, 이를 통해 시스템의 장애 확산을 막고 장애 복구를 도와주는 기능을 제공합니다. 위 그림과 같이 A가 B를 호출할 때, B가 반복적으로 실패한다면 CircuitBreaker를 Open 하여 B에 대한 흐름을 차단하는 기능을 제공합니다.&lt;/p&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;CircuitBreaker를 지원하는 라이브러리&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;Netflix Hystrix (deprecated)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Netflix 에서 개발한 라이브러리로 MSA 환경에서 서비스 간 통신이 원활하지 않을 경우 각 서비스가 장애 내성과 지연 내성을 갖게 하는 라이브러리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Resilience4j
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Netflix Hystrix로 부터 영감을 받아 개발된 Fault Tolerance Library로 Java 전용으로 개발된 경량 라이브러리&lt;/li&gt;
&lt;li&gt;CircuitBreaker, Bulkhead, RateLimiter, Retry, TimeLimiter 등의 여러 가지 코어 모듈이 존재합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상태&lt;/h2&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;1228&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LDxCB/btsGqtnqUDS/asoKwelGwwr4LzN77rqdV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LDxCB/btsGqtnqUDS/asoKwelGwwr4LzN77rqdV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LDxCB/btsGqtnqUDS/asoKwelGwwr4LzN77rqdV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLDxCB%2FbtsGqtnqUDS%2FasoKwelGwwr4LzN77rqdV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1228&quot; height=&quot;664&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;664&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;CircuitBreaker는 3가지 상태가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;closed : 정상
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정상적인 상태로 임계치가 넘어가면 OPEN 상태로 변경됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;open : 장애
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장애 상태로 외부 요청을 차단하고 예외를 발생시키거나 fallback 함수를 호출합니다.&lt;/li&gt;
&lt;li&gt;장애 상태 판단 기준
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;slow call : 기준보다 오래 걸린 요청&lt;/li&gt;
&lt;li&gt;failure call : 실패 혹은 오류 응답을 받은 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;helf open : 장애 이후 임계치 재측정 상태
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;open 상태가 된 이후 일정 요청 횟수/시간이 지난 뒤 open/closed 중 어떤 상태로 변경할지에 대한 판단이 다시 이뤄지는 상태입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CircuitBreaker는 호출 결과를 저장하고 집계하기 위해 슬라이딩 윈도우를 사용합니다. 슬라이딩 윈도우는 마지막 N번의 호출 결과를 기반으로 하는 count-based sliding window(횟수 기반 슬라이딩 윈도우)와 마지막 N초의 결과를 기반으로 하는 time-based sliding window(시간 기반 슬라이딩 윈도우)가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느린 호출율과 호출 실패율이 서킷브레이커에 설정된 임계값보다 크거나 같다면 closed에서 open으로 상태가 변경됩니다. 모든 예외 발생은 실패로 간주(특정 예외만 예외 목록으로 지정하거나 ignore 등록 가능)됩니다. 일정 호출 수가 기록된 후에 느린 호출율과 호출 실패율이 계산됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CircuitBreaker는 서킷이 open 상태라면 CallNotPermittedException을 발생시킵니다. 그리고 특정 시간이 지나면 half open 상태로 바뀌고 설정된 수의 요청을 허용하여 동일하게 느린 호출율과 실패율에 따라 서킷의 상태를 open 또는 closed로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4J는 일반 CircuitBreaker의 3가지 상태에 DISABLED와 FORCED_OPEN 이라는 2가지 상태를 추가로 지원합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;disabled : 서킷브레이커를 비활성화하여 항상 요청을 허용&lt;/li&gt;
&lt;li&gt;forced open : 강제로 서킷을 열어 항상 요청을 거부하는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Resilience4j Property 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;close -&amp;gt; open&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;failureRateThreshold
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : 50&lt;/li&gt;
&lt;li&gt;실패율 임계치 백분율로 해당 값을 넘어갈 경우 open 상태로 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;slowCallDurationThreshold
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : 600000 ms&lt;/li&gt;
&lt;li&gt;해당 설정값을 넘어서는 경우 slow call로 판단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;slowCallRateThreshold
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : 100&lt;/li&gt;
&lt;li&gt;slow call 임계값 백분율로 넘어가면 open 상태로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;open -&amp;gt; half open&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;waitDurationInOpenState
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : 600000ms&lt;/li&gt;
&lt;li&gt;open 상태에서 half open 상태로 변경 대기 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;automaticTransitionFromOpenToHalfOpenEnabled
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : false&lt;/li&gt;
&lt;li&gt;true이면 시간 동안 대기하지 않고 half open으로 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;half open&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;permittedNumberOfCallsInHalfOpenState
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;half open 상태일 때 허용할 call 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;half open -&amp;gt; open&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;maxWaitDurationInHalfOpenState
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : 0&lt;/li&gt;
&lt;li&gt;half open 상태에서 open 상태로 변경되기 전까지 최대 유지 시간&lt;/li&gt;
&lt;li&gt;0인 경우 일부 허용된 call이 완료될 때까지 대기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sliding window&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;slidingWindowType
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : COUNT_BASED&lt;/li&gt;
&lt;li&gt;요청 결과를 기록할 sliding window 타입으로 COUNT_BASED, TIME_BASED 중 선택&lt;/li&gt;
&lt;li&gt;count based는 slidingWindowSize 요청 중 실패율이 설정된 임계값을 초과하면, time based는 slidingWindowSize 시간 동안 실패율이 설정된 임계값을 초과하면 서킷브레이커가 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;slidingWindowSize
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : 100&lt;/li&gt;
&lt;li&gt;sliding window 크기로 count_based 인 경우 개수, time_Based인 경우 초&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;minimumNumberOfCalls
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : 100&lt;/li&gt;
&lt;li&gt;failureRate, slowCallRate 비율을 계산하기 위한 최소 call 개수&lt;/li&gt;
&lt;li&gt;기본값이 100이라면 99번까지 실패해도 circuitBreaker가 동작하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;recordExceptions
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패로 기록할 Exception 리스트 (기본값: empty)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ignoreExceptions
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패나 성공으로 기록하지 않을 Exception 리스트 (기본값: empty)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;recordFailurePredicate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : throwable -&amp;gt; true&lt;/li&gt;
&lt;li&gt;failure로 집계할 exception인지 판단할 predicate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ignoreExceptionPredicate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값 : throwable -&amp;gt; false&lt;/li&gt;
&lt;li&gt;failure로 집계하지 않을 exception인지 판단할 predicate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;spring 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j&quot;)
implementation(&quot;org.springframework.boot:spring-boot-starter-aop&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aop와 resilience4j 의존성을 추가합니다. aop는 annotation 방식을 사용하기 위해서 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# @CircuitBreaker name에 지정된 서킷브레이커가 없으면 default 설정을 가져온 해당 이름의 서킷브레이커를 만든다.
resilience4j:
  circuitbreaker:
    configs:
      default:
        minimum-number-of-calls: 5   # 집계에 필요한 최소 호출 수
        sliding-window-size: 5   # 서킷 CLOSE 상태에서 N회 호출 도달 시 failureRateThreshold 실패 비율 계산
        failure-rate-threshold: 10   # 실패 10% 이상 시 서킷 오픈
        slow-call-duration-threshold: 500   # 500ms 이상 소요 시 실패로 간주
        slow-call-rate-threshold: 10   # slowCallDurationThreshold 초과 비율이 10% 이상 시 서킷 오픈
        wait-duration-in-open-state: 10000   # OPEN -&amp;gt; HALF-OPEN 전환 전 기다리는 시간
        permitted-number-of-calls-in-half-open-state: 5   # HALFOPEN -&amp;gt; CLOSE or OPEN 으로 판단하기 위해 호출 횟수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@CircuitBreaker 애노테이션 방식&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class ArticleController(
    private val articleService: ArticleService
) {

    // fallback은 본 함수와 인자가 일치해야함.
    @CircuitBreaker(name = &quot;article&quot;, fallbackMethod = &quot;failSample&quot;)
    @GetMapping(&quot;/v1/articles/{id}&quot;)
    fun getSampleArticle(): Article {
        val list = listOf(
            IllegalStateException(&quot;illegalState&quot;),
            IllegalArgumentException(&quot;illegalArgument&quot;),
        )
        throw list.random()
    }

    // IllegalArgumentException이 발생했을 경우 호출
    private fun failSample(throwable: IllegalArgumentException): Article {
        return Article(&quot;IllegalArgumentException title&quot;, &quot;IllegalArgumentExceptionfail body&quot;)
    }

    // IllegalStateException이 발생했을 경우 호출
    private fun failSample(e: IllegalStateException): Article {
        return Article(&quot;IllegalStateException title&quot;, &quot;IllegalStateExceptionfail body&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@circuitBreaker의 name 속성의 값으로 application.yml에 설정이 등록되어있지 않다면 application.yml의 default 옵션 세팅으로 name 속성의 circuitBreaker가 생성됩니다. fallbackMethod 속성에 메서드명를 명시하면 특정 예외가 발생했을 때, 호출될 메서드를 지정하여 응답을 대신할 수 있습니다. 서킷이 open 상태로 바뀌면 더 이상 요청은 전달되지 않고 차단되며 CallNotPermittedException 예외가 발생합니다. 이 경우에도 마찬가지로 CallNotPermittedException을 받아서 처리하는 failSample 함수를 구현해서 처리할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애노테이션 방식 개선하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@CircuitBreaker 애노테이션 방식에서는 아래와 같은 문제점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;런타임 예외 가능성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fallbackMethod 속성의 값을 잘못 명시하더라도 컴파일 시점에 알 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;낮은 응집도
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패 시, 여러 fallback 중 어떤 fallback이 동작하는지는 메서드명을 보고 찾아야 하기 때문에 한눈에 들어오지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;구현체를 알아야만 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;circuitBreaker가 open되었을 때 발생하는 CallNotPermittedException 예외가 resilience4j에서 만든 예외이기 때문에 circuitBreaker를 사용하는 함수 입장에서 resilience4j 구현체를 직접 알아야만 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;open fallback 처리의 번거로움
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서킷이 open 되었을 때뿐만 아니라, 함수에서 예외가 발생하면 항상 fallback으로 넘어오기 때문에 서킷 open으로 넘어온 것인지 일반적인 예외로 넘어온 것인지 확인하는 과정이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동일한 클래스의 내부 함수 호출 불가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spring aop가 가지고 있는 일반적인 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개선하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface CircuitBreaker {
    fun &amp;lt;T&amp;gt; run(name: String, block: () -&amp;gt; T): Result&amp;lt;T&amp;gt;
}

@Component
class DefaultCircuitBreaker(
    private val factory: CircuitBreakerFactory&amp;lt;*, *&amp;gt;,
) : CircuitBreaker {

    override fun &amp;lt;T&amp;gt; run(name: String, block: () -&amp;gt; T): Result&amp;lt;T&amp;gt; = runCatching {
        factory.create(name).run(block) { e -&amp;gt; throw e.convertToCustomException() }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DefaultCircuitBreaker의 역할
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spring에서 제공하는 circuitBreakerFactory는 create 메서드로 circuitBreaker 인스턴스를 만들고, run 메서드에 실행할 함수를 인자로 줄 수 있습니다. 두 번째 인자로 예외가 발생했을 경우 처리할 함수를 지정해 줄 수도 있습니다.&lt;/li&gt;
&lt;li&gt;DefaultCircuitBreaker 클래스의 목적은 spring에서 제공하는 CircuitBreakerFactory를 직접 사용하지 않고 한번 감추기 위함입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Result 타입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리턴타입을 Result 클래스로 한번 감싸는 이유는 이후 구현할 fallback에서 체이닝을 하기 위해서입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;convertToCustomException
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CircuitBreaker를 사용하는 곳에서는 resilience4j 구현체를 몰라도 되도록 resilience4j 예외인 CallNotPermittedException를 다른 customException으로 변경하는 역할을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class CircuitBreakerProvider(
    circuitBreaker: CircuitBreaker,
) {
    init {
        Companion.circuitBreaker = circuitBreaker
    }

    companion object {
        private lateinit var circuitBreaker: CircuitBreaker
        fun get() = circuitBreaker
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class CircuitBreakerConfig {

    @Bean
    fun circuitBreakerProvider(
        circuitBreaker: CircuitBreaker,
    ) = CircuitBreakerProvider(circuitBreaker)
}&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;CircuitBreakerProvider 클래스의 역할은 spring application이 뜰 때, circuitBreaker singleton 인스턴스를 하나 받아서 가지고 있다가 필요할 때 전달해 주는 용도입니다. 이후 circuit util 함수를 구현하기 위해서 circuitBreaker 인스턴스를 전달해주는 역할을 합니다.&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;이제 애노테이션 방식의 문제점을 해결할 circuitBreaker util 기능을 구현할 차례입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;/**
 * @param name 서킷 브레이커의 이름으로, 서킷 브레이커 인스턴스를 구별하는 데 사용
 * @param circuitBreaker 실행할 함수를 보호할 서킷 브레이커 인스턴스
 * @param f 실행할 함수. 이 함수는 서킷 브레이커의 하위에서 실행
 */
fun &amp;lt;T&amp;gt; circuit(
    name: String = &quot;default&quot;,
    circuitBreaker: CircuitBreaker = CircuitBreakerProvider.get(),
    f: () -&amp;gt; T,
): Result&amp;lt;T&amp;gt; = circuitBreaker.run(name, f)&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 : circuitBreaker 인스턴스를 구별하는 데 사용되는 서킷 브레이커 이름을 명시합니다.&lt;/li&gt;
&lt;li&gt;circuitBreaker : 실행할 함수를 보호할 서킷 브레이커 인스턴스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞서 spring application이 뜰 때, circuitBreaker에서 singleton 인스턴스를 하나 받아서 가지고 있는 이유가 이 util 클래스에서 사용하기 위함입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;f : circuitBreaker에 감싸져서 실행될 실제 target 함수입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class CircuitOpenException(message: String = &quot;Circuit breaker is open&quot;) : RuntimeException(message)

fun Throwable.convertToCustomException(): Throwable = when (this) {
    is CallNotPermittedException -&amp;gt; CircuitOpenException()
    else -&amp;gt; this
}

fun &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt;.fallback(f: (e: Throwable?) -&amp;gt; T): Result&amp;lt;T&amp;gt; = when (this.isSuccess) {
    true -&amp;gt; this
    false -&amp;gt; runCatching { f(this.exceptionOrNull()) }
}

fun &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt;.fallbackIfOpen(f: (e: Throwable?) -&amp;gt; T): Result&amp;lt;T&amp;gt; = when (this.exceptionOrNull()) {
    is CircuitOpenException -&amp;gt; runCatching { f(this.exceptionOrNull()) }
    else -&amp;gt; this
}&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;convertToCustomException
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞서 DefaultCircuitBreaker 클래스 정의 부분에서 설명한 CallNotPermittedException 예외를 CustomException으로 변경하는 함수입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CircuitOpenException
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CallNotPermittedException를 대신할 custom exception입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fallback
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공인 경우에는 그대로 리턴하고, 실패한 경우에는 전달받은 fallback용 block을 실행하고 result로 감싸서 응답합니다.&lt;/li&gt;
&lt;li&gt;즉, 실패 예외가 어떤 것이든 fallback 동작을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fallbackIfOpen
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Result에서 꺼낸 exception이 CircuitOpenException인 경우에만 전달받은 fallback block을 실행하고 이외의 경우에는 result를 그대로 응답합니다.&lt;/li&gt;
&lt;li&gt;즉, circuitBreaker가 Open인 경우에만 fallback으로 전달한 동작이 수행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 사용 예시
@RestController
class CircuitUtilController() {

    @GetMapping(&quot;/util/articles/fallback&quot;)
    fun getFallbackSampleArticle(): Article {
        return circuit(&quot;fallback-article&quot;) {
            throw RuntimeException(&quot;runtime&quot;)
        }.fallback {
            Article(&quot;Fallback title&quot;, &quot;Fallback body&quot;)
        }.getOrThrow()
    }

    @GetMapping(&quot;/util/articles/open&quot;)
    fun getFallbackOpenSampleArticle(): Article {
        return circuit(&quot;fallback-open-article&quot;) {
            throw RuntimeException(&quot;runtime&quot;)
        }.fallbackIfOpen {
            Article(&quot;Fallback Open Default title&quot;, &quot;Fallback Open Default body&quot;)
        }.getOrThrow()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* &lt;a href=&quot;https://www.youtube.com/watch?v=ThLfHtoEe1I&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;서킷브레이커 사용 방식 개선하기&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>CircuitBreaker</category>
      <category>resilience4j</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/83</guid>
      <comments>https://backtony.tistory.com/83#entry83comment</comments>
      <pubDate>Sun, 7 Apr 2024 18:04:56 +0900</pubDate>
    </item>
    <item>
      <title>Elasticsearch - 샤드, 인덱스 운영 전략</title>
      <link>https://backtony.tistory.com/82</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;샤드 운영 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤드는 다음과 같은 옵션으로 개수를 설정할 수 있습니다.&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;number_of_shard : 프라이머리 샤드 개수&lt;/li&gt;
&lt;li&gt;number_of_replicas : 레플리카 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 인덱스에 위 옵션을 3, 2로 지정했다면 해당 인덱스는 프라이머리 샤드 3개에 대한 복제본이 2개씩 생기므로 프라이머리 3개, 레플리카 6개로 총 9개의 샤드가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;number_of_shard는 인덱스가 데이터를 몇 개의 샤드로 쪼갤 것인지를 지정하는 값이므로 신중하게 설계해야 합니다. 한 번 지정하면 reindex 같은 동작을 통해 인덱스를 통째로 재색인하는 등 특별한 작업을 수행하지 않는 한 바꿀 수 없기 때문입니다. 샤드 개수를 어떻게 지정하느냐는 엘라스틱서치 성능에도 영향을 미칩니다. 클러스터에 샤드 숫자가 너무 많아지면 클러스터 성능이 떨어지나 인덱스당 샤드 숫자를 적게 지정하면 샤드 하나의 크기가 커집니다. 샤드 크키가 커지면 장애 상황 등에서 샤드 복구에 많은 시간이 소요되므로 클러스터 안정성이 떨어지게 됩니다. 공식 문서는 다음과 같은 가이드를 권장합니다.&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;샤드 하나의 크기는 10GB ~ 40GB&lt;/li&gt;
&lt;li&gt;노드 heap 1GB당 20개 이하의 shard 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤드 하나의 크기는 20GB만 되어도 느리다는 감각이 느껴지므로 보통 수 GB 내외로 조정하고 32g heap 기준으로 노드당 640개의 샤드를 가지게 되는데 이보다는 조금 더 적은 샤드를 갖는 구성을 하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 대수가 n 대라면 number_of_shards를 n 배수로 지정해 모든 노드가 작업을 고르게 분산받도록 설정할 수 있습니다. 서비스 중요도가 높은 인덱스나 성능을 타이트하게 조정해야 하는 인덱스라면 이 부분을 고려해서 값을 지정해야 합니다. 하지만 모든 인덱스를 이렇게 처리할 필요는 없습니다. 추후 선형적 확장을 위해 서버가 추가 투입되면 공식이 깨지기 때문입니다. 또한 엘라스틱서치 클러스터에서 활발하게 작업 중인 인덱스의 수가 충분히 많다면 단일 인덱스에 대해 모든 노드가 일을 하고 있는지를 과도하게 신경 쓸 필요가 없을 수도 있습니다. 한 인덱스에 참여하지 않는 노드이더라도 다른 인덱스의 작업에 리소스를 사용할 것이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구축 시나리오 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일 100GB 데이터 분석용 클러스터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&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;하루에 100GB 정도의 데이터를 저장하면서 보관 기간이 한 달인 분석 엔진 클러스터&lt;/li&gt;
&lt;li&gt;인덱스 이름 패턴 : es-YYYY-MM-dd&lt;/li&gt;
&lt;li&gt;프라이머리 샤드 기준 하루에 색인되는 인덱스 용량 : 100GB * 1일&lt;/li&gt;
&lt;li&gt;인덱스 보관 기간 : 30일&lt;/li&gt;
&lt;li&gt;레플리카 샤드 개수 : 1개&lt;/li&gt;
&lt;li&gt;클러스터에 저장되는 전체 예상 용량 : 100GB * 30(일) * 2(레플리카, 프라이머리)&lt;/li&gt;
&lt;li&gt;데이터 노드 한대에 저장 가능한 용량 : 2TB&lt;/li&gt;
&lt;li&gt;클러스터에 저장될 인덱스의 총 개수 : 30개
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하루에 한개의 인덱스가 생성되므로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6TB가 최대 필요 용량으로 예상되어 노드를 3개만 두어 맞추면 안 됩니다. es는 기본적으로 노드의 디스크 사용률을 기준으로 샤드를 배치하기 때문에 노드의 디스크 사용률이 low watermark 기본값인 85%가 넘으면 해당 노드에 샤드 할당을 지양한다. 따라서 노드의 최대 데이터 적재 용량을 80%로 잡고 시나리오를 작성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구축&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 노드 5대
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;6TB의 용량이 전체 용량의 80%여야 하므로 전체 용량을 7.5TB로 산정해야 합니다.&lt;/li&gt;
&lt;li&gt;데이터 노드 한 대가 장애가 발생했다고 가정했을때, 레플리카 샤드로 인해 한대의 노드 장애에 대해서는 클러스터의 yellow 상태를 보장합니다. 하지만 오랜 기간 장애가 이어질 수 있으므로 해당 노드에 저장된 문서들을 다른 노드에서 충분히 받아줄 수 있을 만큼의 용량을 확보해야 합니다.&lt;/li&gt;
&lt;li&gt;데이터 노드가 4대일 때에는 총 8TB가 확보되어 6TB의 데이터를 수용하는데 문제가 없지만 노드 한대에서 장애가 발생해서 클러스터에 노드가 3대만 남게 되면 모든 데이터 노드의 디스크가 가득 차게 됩니다. 하지만 5대로 구성하면 총 10TB가 확보되어 한대가 장애가 발생하더라고 8TB까지 수용할 수 있어 장애 복구가 시급하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;프라이머리 샤드 10개
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;es에서는 샤드 하나의 크기를 20~40G 정도로 할당을 권고합니다.&lt;/li&gt;
&lt;li&gt;노드 간 볼륨 사용량 불균형을 막기 위해 데이터 노드의 n배로 샤드의 개수를 산정해야 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터노드가 5대이므로 2배인 10개의 프라이머리 샤드를 구축한다.&lt;/li&gt;
&lt;li&gt;10개 * 30일 * 2(레플리카)를 하면 총 600개의 샤드가 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스 하나를 기준으로 노드 한 대에 할당되는 샤드 개수 : 10 * 2(레플리카) / 5(노드수) = 4개&lt;/li&gt;
&lt;li&gt;인덱스 전체를 기준으로 노드 한 대에 할당되는 샤드의 총 개수 : 10 * 2(레플리카) * 30일 / 5(노드수) = 120개&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 노드의 개수가 5개이므로 2 배수인 10개의 프라이머리 샤드 개수로 산정한다면, 하루에 색인되는 용량은 총 100GB이므로 10개의 프라이머리 샤드로 구성된다면 샤드 하나의 크기는 10GB 정도로 할당됩니다. 하루에 레플리카 샤드를 포함하여 총 20개의 샤드를 생성하게 되고, 데이터 노드 5대가 샤드를 4개씩 나눠 갖게 됩니다. 30일이 지나면 600개의 샤드가 생성되고, 데이터 노드 5대가 샤드를 120개씩 나눠갖게 됩니다.&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;일 1GB의 데이터 분석과 장기간 보관용 클러스터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&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;하루에 1GB 정도의 데이터를 저장하면서 보관 기간이 3년인 분석 엔진 클러스터&lt;/li&gt;
&lt;li&gt;인덱스 이름 패턴 : es-YYYY-MM-dd&lt;/li&gt;
&lt;li&gt;프라이머리 샤드 기준 한 달에 색인되는 인덱스의 용량 : 1gb * 30 = 30GB&lt;/li&gt;
&lt;li&gt;인덱스 보관 기간 : 3년&lt;/li&gt;
&lt;li&gt;레플리카 개수 : 1개&lt;/li&gt;
&lt;li&gt;클러스터에 저장되는 전체 예상 용량 : 30GB * 36개월 * 2(레플리카) = 2.16TB&lt;/li&gt;
&lt;li&gt;데이터 노드 한 대에 할당할 수 있는 용량 : 2TB&lt;/li&gt;
&lt;li&gt;클러스터에 저장될 인덱스의 총 개수 : 36개&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구축&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 노드 3대
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;용량은 노드 2대면 충분하지만 장애의 경우를 대비해 3대로 구축&lt;/li&gt;
&lt;li&gt;3대를 구축하니 용량의 여유분이 많이 발생하므로 레플리카를 2로 수정하여 안정성을 높일 수 있습니다.&lt;/li&gt;
&lt;li&gt;30GB * 36개월 * 3(레플리카) = 3.24TB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스를 구성하는 프라이머리 샤드 개수 : 6개
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 하나당 할당되는 샤드 개수 : 6 * 3 = 18개&lt;/li&gt;
&lt;li&gt;총 샤드 개수 : 6개(샤드) * 36(개월) * 3(레플리카) = 648개&lt;/li&gt;
&lt;li&gt;한 노드에 할당되는 샤드 수 : 648 / 3 = 216개&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 노드의 개수가 3개이므로 2배수인 6개를 프라이머리 샤드 개수로 산정한다면, 한 달에 에 색인되는 용량은 총 30GB이므로 6개의 프라이머리 샤드로 구성된다면 샤드 하나의 크기는 5GB 정도로 할당됩니다. 한 달에 레플리카 샤드를 포함하여 총 18개의 샤드를 생성하게 되고, 데이터 노드 3대가 샤드를 6개씩 나눠 갖게 됩니다. 36개월이 지나면 648개의 샤드가 생성되고, 데이터 노드 3대가 샤드를 216개씩 나눠갖게 됩니다.&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;일 100GB의 데이터 분석과 장기간 보관 클러스터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&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;하루에 100GB 이상의 큰 데이터를 저장하면서 보관 기간이 1년인 클러스터&lt;/li&gt;
&lt;li&gt;인덱스 이름 패턴 : es-YYYY-MM-dd&lt;/li&gt;
&lt;li&gt;프라이머리 샤드 기준 하루에 색인되는 인덱스 용량 : 100GB&lt;/li&gt;
&lt;li&gt;인덱스 보관 기간 : 1년&lt;/li&gt;
&lt;li&gt;레플리카 샤드 개수 : 1개&lt;/li&gt;
&lt;li&gt;클러스터에 저장되는 전체 용량 : 100GB * 365일 * 2(레플리카) = 73TB&lt;/li&gt;
&lt;li&gt;데이터 노드 한 대에 할당할 수 있는 용량 : 2TB&lt;/li&gt;
&lt;li&gt;클러스터에 저장될 인덱스 총 개수 : 365개&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시나리오대로라면 디스크 저장 공간이 총 73TB가 필요하고, 데이터 노드가 37대는 되어야 모든 용량을 저장할 수 있습니다. 장애 상황까지 고려하면 이보다 훨씬 많은 노드가 필요합니다. 보통 이렇게 오랜 기간 저장하는 데이터는 모든 데이터를 자주 분석하지 않습니다. 최근 1일, 1주, 1개월 혹은 3개월 등의 기준으로 데이터를 조회하며, 1년 치 데이터를 조회하는 경우는 일 년에 몇 회 정도입니다. 이렇게 1년에 몇 번 조회하지 않는 데이터를 위해 많은 비용을 지불하는 낭비를 막기 위해 데이터 노드를 hot/warm data 형태로 구성할 수 있습니다. 자주 조회하게 될 최근 데이터는 hot 영역, 자주보지 않지만 연간 분석을 위해 가끔 조회하게 될 데이터는 warm에 저장하는 방식을 취하면 데이터 보관 요구와 비용절감을 모두 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 빠른 응답을 위해 SSD 디스크 사용을 권고하지만 warm 구성에 사용할 디스크는 상대적으로 저렴하면서 고용량의 저장 공간을 제공하는 SATA 디스크를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구축&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hot 노드 인덱스 보관 기간 : 1개월&lt;/li&gt;
&lt;li&gt;hot 노드에 저장되는 전체 예상 용량 : 100GB * 2(레플리카) * 30일 = 6TB&lt;/li&gt;
&lt;li&gt;warm 노드 한 대에 할당 가능한 용량 : SATA 10TB&lt;/li&gt;
&lt;li&gt;warm 노드 인덱스 보관 기간 : 11개월&lt;/li&gt;
&lt;li&gt;warm 노드 저장되는 전체 예상 용량 : 100GB * 2(레플리카) * 335일 = 67TB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 조건을 바탕으로 노드 산정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hot 노드 : 5대
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;6TB/10TB * 100 = 60%&lt;/li&gt;
&lt;li&gt;한 대 노드 장애시, 6TB/8TB = 75%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;warm 노드 : 10대
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;67TB/100TB * 100 = 67%&lt;/li&gt;
&lt;li&gt;한 대 노드 장애시, 67TB/90TB * 100 = 74%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 조건으로 샤드 개수 산정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프라이머리 샤드 개수 : 10개 (hot 노드의 n배)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클러스터 전체 인덱스에 의해 생성되는 샤드 총 개수 : 10 * 365일 * 2(레플리카) = 7300개&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;hot 노드 한 대에 할당되는 샤드 총 개수 : 10 * 2(레플리카) * 30일 / 5대 = 120개&lt;/li&gt;
&lt;li&gt;warm 노드 한 대에 할당되는 샤드 총 개수 : 10 * 2(레플리카) * 335일 / 10대 = 670개&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 hot과 warm 노드의 개수가 다르기 때문에 샤드 개수를 잘 산정하지 않으면 hot에 있을 때 균등하게 배치된 샤드가 warm으로 넘어갈 때 불균등하게 배치될 수 있습니다. 이때 재분배되더라도 균등하게 분배되게 하려면 hot 노드의 개수와 warm 노드 개수의 최소 공배수로 샤드를 설정하여 인덱스를 생성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 시나리오에서 hot은 5대, warm은 10대의 최소 공배수는 10입니다. 하루에 샤드는 총 20(10+10)개가 생성되고 노드 5대에 각 4개씩 샤드가 분배됩니다. 한 달이 지나고 warm 노드로 이동할 때는 10대에 각 2대씩 분배하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 설정은 다음 세가지 사항을 지정해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 노드에서 해당 노드를 hot으로 사용할지, warm 노드로 사용할지&lt;/li&gt;
&lt;li&gt;최초 인덱스 생성 시 hot 노드로 샤드가 할당될 수 있도록 설정&lt;/li&gt;
&lt;li&gt;한 달이 지난 이후 인덱스의 설정을 warm 노드로 할당하도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 분석 엔진에 유용한 성능 확보 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;색인이 끝난 인덱스는 forcemerge api로 검색 성능 확보&lt;/li&gt;
&lt;li&gt;색인이 끝난 인덱스는 read only로 설정하여 shard request cache가 초기화되어 삭제되지 않도록 설정&lt;/li&gt;
&lt;li&gt;한 달이 지난 인덱스는 hotdata 노드에서 warmdata 노드로 샤드 재배치&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;검색 엔진으로 활용하는 클러스터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;100ms 내에 검색 결과가 제공되어야 한다.&lt;/li&gt;
&lt;li&gt;검색 엔진에 사용할 데이터는 500GB&lt;/li&gt;
&lt;li&gt;검색 요구사항이 변경되어 매핑이 변경될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구축&lt;/b&gt;&lt;br /&gt;검색 엔진에 사용되는 데이터는 보통 준비된 상태에서 서비스하며, 인덱스의 필드가 추가되거나 필드 데이터 타입이 변경되는 등의 매핑 변경이 있을 때 인덱스를 새롭게 생성 후 재색인하여 운영합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;es는 기본적으로 1쿼리 1샤드 1스레드를 기준으로 검색 요청을 처리합니다. 각 노드에 샤드가 1개씩 있고, 3대의 노드(A, B, C)에는 4개의 cpu 코어가 있다고 가정해 봅시다. 사용자의 검색 요청을 받은 노드 C가 검색 스레드 풀에서 스레드 하나를 꺼내 자신이 가지고 있는 샤드에 검색 요청에 해당하는 문서가 있는지 찾아보면서 동시에 A, B 노드에 문서가 있는지 찾아달라는 요청을 보냅니다. 쿼리는 모든 노드에 동일하게 요청하며 각각의 노드는 자신의 검색 스레드 풀의 스레드 하나를 사용해서 샤드에서 문서를 검색합니다. 하지만 노드에 샤드가 1개 이상 있다면 검색요청은 스레드 풀에 있는 사용 가능한 스레드를 모두 사용해서 각각의 샤드에서 문서를 찾습니다. 즉, 검색 요청의 처리를 완료하기 전까지 다른 검색 쿼리가 처리되지 못하고 검색 스레드 큐에 위치합니다. 만약 큐가 가득 차면 rejected 현상이 발생하고 샤드가 지나치게 많으면 검색 성능을 저하시키는 원인이 됩니다. 따라서 클러스터를 검색 엔진으로 구축할 경우 성능 테스트를 통해 적정한 수준의 샤드 수를 결정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 다음과 같이 진행합니다. 먼저 데이터 노드 한대로 클러스터를 구성하고, 해당 노드에 데이터를 저장한 후 사용자의 검색 쿼리에 대한 응답을 100ms 이하로 줄 수 있는지를 테스트해야 합니다. 이때 클러스터 구성은 데이터 노드 한대, 레플리카 샤드 없이 프라이머리 샤드만 1개로 구성한다. 그리고 해당 샤드에 데이터를 계속 색인하면서 샤드의 크기가 커짐에 따라 검색 성능이 어떻게 변하는지를 측정합니다. 이렇게 구성해야 데이터 노드가 샤드 하나로 검색 요청을 처리할 때의 성능을 측정할 수 있습니다. 샤드 하나당 하나의 검색 스레드만 사용해야 검색 스레드 큐에 검색 쿼리가 너무 많이 쌓이지 않아서 하나의 샤드에서 측정된 검색 성능을 보장할 수 있기 때문입니다. 위의 요구사항에 맞추려면 took에 100ms 이하로 나오면 요구조건에 만족하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 샤드로 구성되었기 때문에 이때 생성된 인덱스의 크기가 곧 단일 샤드의 크기가 됩니다. 이렇게 노드 한 대가 사용자의 요구인 100ms 속도로 검색 결과를 리턴해줄 수 있는 샤드의 적정 크기를 측정합니다. 일반적으로 샤드의 크기가 커짐에 따라 시간도 증가하는 양의 상관관계를 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스할 전체 데이터 크기를 테스트를 통해 산정한 인덱스의 크기로 나누면 그 값이 사용자가 원하는 응답 속도를 보여줄 수 있는 샤드의 개수가 됩니다. 예를 들어 테스트를 통해 100ms의 응답을 주는 인덱스의 크기가 25GB였을 때, 시나리오상 인덱스 크기 500GB를 25GB로 나누어 샤드 개수를 20개로 산정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤드 개수를 20개로 정했다면 역으로 데이터 노드 개수를 산정하기 위해서 n개로 나눠보면 데이터 노드 개수는 2, 4, 5, 10개 범위 내에서 선택할 수 있습니다. 데이터 노드 개수를 정했다면 필요한 용량만큼의 데이터 노드 서버 스팩을 선택할 수 있습니다.&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;인덱스 패턴 search_index_v1&lt;/li&gt;
&lt;li&gt;프라이머리 샤드 기준 하루에 색인되는 인덱스 용량 : 500GB&lt;/li&gt;
&lt;li&gt;인덱스 보관 기간 : 재색인 요구가 있을 때까지 보관&lt;/li&gt;
&lt;li&gt;레플리카 샤드 개수 : 1개&lt;/li&gt;
&lt;li&gt;클러스터에 저장되는 전체 예상 용량 : 500GB * 2(레플리카) : 1TB&lt;/li&gt;
&lt;li&gt;클러스터의 전체 인덱스에 의해 생성되는 샤드 총 개수 : 20 * 2(레플리카) = 40개&lt;/li&gt;
&lt;li&gt;데이터 노드 개수 : 20 / n =&amp;gt; 2, 4, 5, 10 개 중 선택 =&amp;gt; 4 선택&lt;/li&gt;
&lt;li&gt;인덱스 하나를 기준으로 데이터 노드 한 대에 할당되는 샤드 개수 : 20 * 2 / 4 = 10개&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100ms 응답을 주는 인덱스의 크기가 25GB였을 때를 기준으로 지정했지만 레플리카로 인해 샤드 개수가 늘어났고, 데이터 노드 개수를 늘렸으로 더 좋은 성능이 나오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터를 구성하는 비용 또한 중요한 문제이기 때문에 실제로는 적절하게 조절해야 합니다. 과하다 싶으면 샤드와 노드 개수를 줄이거나 급격한 데이터 증가가 예상된다면 레플리카 샤드 개수를 늘리거나 노드를 추가해서 성능을 확보할 수도 있습니다. 만약 색인이 공존하는 검색 엔진이 아니라면 forcemerge api나 read_only 설정을 적용해서 성능을 확보할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;매일 인덱스가 바뀌는 데이터 분석 엔진&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스의 총 보관 기간 동안 차지하게 될 용량 예측(replicas 개수와 함께 고려된 용량)&lt;/li&gt;
&lt;li&gt;데이터 노드 한대에 저장 가능한 용량 측정&lt;/li&gt;
&lt;li&gt;차지하게 될 총 용량을 1대의 노드가 장애가 난 상태에서도 80% 이하로 구축할 수 있는 데이터 노드 개수 산출&lt;/li&gt;
&lt;li&gt;산출된 데이터 노드 개수 * n배(처음에는 2배)로 프라이머리 노드 개수 지정&lt;/li&gt;
&lt;li&gt;하루 색인되는 용량 / 프라이머리 노드 개수 &amp;lt;= 20GB로 프라이머리 개수 구하는 n배수를 조정 (10GB 이내로 맞출 수 있다면 맞추기)&lt;/li&gt;
&lt;li&gt;총 샤드 개수(레플리카 + 프라이머리) / 데이터 노드 &amp;lt;= 640 개로 만족하지 않으면 노드를 추가하거나 샤드 개수 n배수 조정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장기간 보관 데이터의 경우, hot, warm 아키텍처를 고려해볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색 엔진(데이터가 불려진다는 가정)&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;검색 쿼리에 대한 응답 ms 기준 측정(ex. 100ms 이하로 응답)&lt;/li&gt;
&lt;li&gt;클러스터 구성(데이터 노드 1대, 레플리카 샤드 X, 프라이머리 샤드 1대)으로 쿼리 응답 ms 기준을 만족할 때까지 데이터를 계속 색인하여 적정한 하나의 샤드 크기를 결정 =&amp;gt; 단일 샤드로 구성되었으므로 인덱스의 크기가 곧 단일 샤드의 크기&lt;/li&gt;
&lt;li&gt;실제 서비스할 전체 데이터 크기 / 앞서 측정한 단일 샤드의 크기 = 사용자가 원하는 응답 속도를 보여줄 수 있는 샤드의 개수&lt;/li&gt;
&lt;li&gt;샤드 개수 / n = 선택할 수 있는 데이터 노드 개수 범위&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색엔진은 예상되는 크기의 전체 디스크 사용량이 크게 중요하지 않기 때문에 성능을 보고 데이터 노드와 레플리카 샤드 개수를 조정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스 운영 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;템플릿과 명시적 매핑 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스에서는 동적으로 생성되는 것보다는 최대한 명시적 매핑을 지정하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 지정은 성능을 유의미하게 상승시키므로 사전에 서비스 요건과 데이터 특성 등을 파악하고 어떤 값을 라투잉으로 지정해야 할지 설계해야 합니다. 라우팅을 지정하기로 했다면 해당 인덱스에 접근하는 클라이언트도 라우팅 정책 내용을 숙지하고 있어야 하며 이를 위해 인덱스 매핑에서 _routing을 true로 지정해 라우팅 지정을 필수로 제한하는 방법을 검토해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시계열 인덱스 이름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시계열 데이터를 색인한다면 인덱스 이름에도 시간값을 넣는 것이 좋습니다. 이런 방법을 채택하면 오래된 데이터를 백업하고 삭제하는 것이 편리하며 데이터 노드를 티어로 구분해서 클러스터를 구성하는 데이터 티어 구조에도 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;alias&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;alias는 이미 존재하는 인덱스를 다른 이름으로 가리키도록 하는 기능입니다. alias가 하나 이상의 인덱스를 가리키도록 지정할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST _aliases
{
  &quot;actions&quot;: [
    {
      &quot;add&quot;: {
        &quot;index&quot;: &quot;my_index&quot;,
        &quot;alias&quot;: &quot;my_alias_name&quot;
      }
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 여러 인덱스를 가리키는 alias라면 단일 문서 조회 작업의 대상이 될 수 없습니다. 업데이트, 삭제 등 쓰기 작업의 경우 is_write_index를 true로 지정한 인덱스를 대상으로 작업되고 없으면 쓰기가 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 로그성 데이터가 아니라 서비스에 직접 활용되는 데이터를 들고 있는 인덱스라면 모두 alias를 사전에 지정하는 것이 중요합니다. 사전에 정의해두면 나중에 매핑이나 설정 등에 변화가 필요할 때 시 인덱스를 미리 만들고 alias가 가리키는 인덱스만 변경하면 운영 중에 새 인덱스로 넘어갈 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;롤오버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 alias 에 여러 인덱스를 묶고 한 인덱스에만 is_write_true를 지정하면 쓰기를 담당하는 인덱스 내 샤드 크기가 커지면 새로운 인덱스를 생성해 같은 alias 안에 묶고 is_write_true를 새로운 인덱스로 옮기는 방식으로 운영하게 됩니다. 이를 한 번에 묶어서 수행하주는 기능이 롤오버입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;POST [롤오버 대상]/_rollover&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;alias의 이름 또는 데이터 스트림이 롤오버 대상으로 들어갑니다. alias내의 is_write_true 대상 인덱스는 하이픈 숫자 패턴을 따라야 합니다. (ex. test-index-000001) provided_name을 지정하고 대상을 alias로 묶고 롤오버 하면 provided_name 을 이용해서 새로운 인덱스를 생성할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 스트림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 스트림은 내부적으로 여러 개의 인덱스로 구성되어 있습니다. 검색을 수행할 때는 해당 데이터 스트림에 포함된 모든 인덱스를 대상으로 검색할 수 있고, 문서를 추가 색인할 때는 가장 최근에 생성된 단일 인덱스에 새 문서가 들어갑니다. 롤오버 시에는 최근 생성된 인덱스의 이름 끝 숫자를 하나 올린 인덱스가 생성됩니다. 즉, 여러 인덱스를 묶고 is_write_true 인덱스를 하나 둔 alias와 유사하게 동작합니다. 데이터 스트림은 인덱스 템플릿과 연계해서 시계열 데이터 사용 패턴에 맞게 정형화하고 간단하게 사용할 수 있도록 정제된 것이라고 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 스트림을 구성하는 인덱스는 뒷바침 인덱스(backing indices)라고 부르며 모두 hidden 속성입니다. 인덱스 이름 패턴은 고정이고 롤오버 시 명시적인 새 인덱스 이름 지정이 불가능합니다. 패턴은 &lt;b&gt;.ds-데이터 스트림 이름-yyy.MM.dd-세대 수&lt;/b&gt; 형태입니다. 세대수는 000001부터 시작해서 증가하고, 반드시 인덱스 템플릿과 연계되어야 합니다. 문서 추가는 가능하지만 업데이트는 불가능하고 @timestamp 필드가 포함된 문서만 취급합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ILM(index lifecycle management, 인덱스 생명 주기 관리) 정책 연동이 필수는 아니지만 데이터 스트림 기능 자체가 ILM과 연계를 염두에 두고 개발된 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 스트림은 자동으로 많은 것들을 관리해 주는 대신 제약도 크기 때문에 서비스를 운영하다 보면 장애가 발생해서 특정 시간대에 발생한 시계열 데이터를 원하는 대로 온전히 처리하지 못하는 상황이 발생합니다. 데이터 스트림에 데이터는 멱등하게 재처리하기 쉽지 않기 때문에 시스템의 모니터링용 지표 데이터를 수집하기 위한 용도 등 그냥 문제 시간대의 데이터를 버려도 큰 문제가 되지 않는 경우에 사용하기 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 티어 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 티어 구조는 데이터 노드를 용도 및 성능별로 hot-warm-cold-frozen 티어로 구분해서 클러스터를 구분하는 방법입니다. 노드의 역할로 data를 지정하지 않고 data_content, data_hot, data_warm, data_cold, data_frozen을 지정해 클러스터를 구성합니다. 성능 차이가 많이 나는 장비를 가지고 클러스터를 구성해야 하거나 최근 데이터 위주로 사용하는 방식의 시계열 데이터를 운영할 때 채택하기 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 티어 구조를 위해서는 node.roles에 data가 아니라 다른 역할을 지정해야 합니다. &lt;b&gt;node.roles: [&quot;data_content&quot;, &quot;data_hot&quot;]&lt;/b&gt; 와 같이 여러 역할을 겸임해도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;data_content : 시계열 데이터가 아닌 데이터를 담는 노드로 실시간 서비스용 데이터용을 위한 역할입니다. 데이터 티어 구조를 채택한 경우 필수로 필요한 역할입니다.&lt;/li&gt;
&lt;li&gt;data_hot : 시계열 데이터 중 가장 최근 데이터를 담당하는 노드로 잦은 업데이트, 읽기의 데이터를 담당합니다. 데이터 티어 구조를 채택한 경우 필수로 필요한 역할입니다.&lt;/li&gt;
&lt;li&gt;data_warm : hot에 배정된 인덱스보다 기간이 오래된 인덱스를 담당하는 노드로, 상대적으로 수행 능력이 덜 요구되는 인덱스를 배정받습니다.&lt;/li&gt;
&lt;li&gt;data_cold : 더 이상 업데이트를 수행하지 않는 읽기 전용 인덱스를 담당하는 노드입니다.&lt;/li&gt;
&lt;li&gt;data_frozen : 인덱스를 검색 가능한 스냅숏으로 변환 한 뒤 이를 배정받는 노드로 단일 역할만 가지도록 설정하는 것이 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;PUT my_index/_settings
{
  &quot;index.routing.allocation.include._tier_preference&quot;: &quot;data_warm,data_hot&quot; 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 지정할 경우 data_warmn 노드에 인덱스를 할당하고 없는 경우 data_hot에 인덱스를 할당합니다. 명시적으로 null을 지정하면 데이터 티어를 고려하지 않으며 기본값은 data_content이고 데이터 스트림 내 인덱스 생성 시 기본값은 data_hot입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 생명 주기 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ILM은 인덱스를 hot-warm-cold-frozen-delete 페이즈로 구분해서 지정한 기간이 지나면 인덱스를 다음 페이즈로 전환시키로 이때 지정한 작업을 수행하도록 하는 기능입니다.&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;hot : 현재 업데이트가 수행되고 있는 읽기 작업도 가장 많은 상태&lt;/li&gt;
&lt;li&gt;warm : 인덱스에 더 이상 업데이트가 수행되지는 않지만 읽기 작업은 들어오는 상태&lt;/li&gt;
&lt;li&gt;cold : 인덱스에 더이상 업데이트가 수행되지 않고 읽기 작업도 가끔씩 들어오는 상태, 검색은 되나 느려도 되는 상태&lt;/li&gt;
&lt;li&gt;frozen : 인덱스에 더이상 업데이트가 수행되지 않고 읽기 작업도 거의 들어오지 않는 상태, 검색은 되나 느려도 되는 상태&lt;/li&gt;
&lt;li&gt;delete : 인덱스가 삭제되어도 되는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;frozen 페이즈에서는 운영 중인 인덱스를 검색 가능한 스냅숏으로 변환하는 작업이 가능하지만 엔터프라이즈 등급의 구독에서만 사용 가능합니다. 각 페이즈의 설계 의도는 위와 같지만 정책을 적용할 때 꼭 부합하지 않아도 됩니다. warm 페이즈에도 업데이트를 허용하도록 해도 되고 hot페이즈 다음에 바로 delete로 넘어가도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 시나리오가 가능합니다. 인덱스가 처음 생성된 hot 페이즈에서는 매일 자동으로 롤오버를 수행하고 샤드 사이즈가 8GB 크키가 넘어가면 날짜가 넘어가지 않아도 롤오버를 수행하도록 합니다. 생성된 지 3일이 지난 인덱스는 warm 페이즈로 전환하고 해당 인덱스는 읽기 전용으로 바꾸고 단일 세그먼트로 강제 병합합니다. 생성된 지 7일이 지난 인덱스는 cold 페이즈로 전환하고 샤드 복구 우선순위를 낮춥니다. 생성된지 30일이 지난 인덱스는 delete페이즈로 이동시켜 스냅숏 생명 주기 정책에서 스냅샷으로 백업될 때까지 기다렸다가 백업이 완료되면 삭제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페이즈 액션 종류&lt;/b&gt;&lt;br /&gt;각 페이즈마다 지정할 수 있는 페이즈 액션 종류는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;롤오버
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지정된 조건을 만족할 때 롤오버를 수행합니다.&lt;/li&gt;
&lt;li&gt;hot 페이즈에서 수행 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;읽기전용으로 만들기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hot, warm, cold 페이즈에서 수행 가능합니다. hot에서 사용 시, 롤오버 액션과 함께 지정되어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;세그먼트 병합
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세그먼트를 지정한 개수로 강제 병합하고 읽기 전용으로 만듭니다.&lt;/li&gt;
&lt;li&gt;hot, warm 페이즈에서 수행 가능합니다.&lt;/li&gt;
&lt;li&gt;hot 페이즈에서 이 액션을 지정하려면 롤오버 액션과 함께 지정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;shrink
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 읽기 전용으로 만들고 shrink를 수행해서 새 인덱스를 만듭니다.&lt;/li&gt;
&lt;li&gt;새 인덱스의 샤드 개수와 샤드 하나의 최대 크기를 지정해서 개수를 정할 수도 있습니다.&lt;/li&gt;
&lt;li&gt;샤드가 한 노드에 모여 있지 않다면 복사를 수행하고 작업을 진행합니다.&lt;/li&gt;
&lt;li&gt;hot, warm에서 수행 가능합니다.&lt;/li&gt;
&lt;li&gt;hot 페이즈에서 수행하려면 롤오버 액션과 함께 지정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스 우선순위 변경
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스의 복구 우선순위를 변경합니다.&lt;/li&gt;
&lt;li&gt;hot, warm, cold에서 수행가능합니다.&lt;/li&gt;
&lt;li&gt;값이 클수록 우선적으로 복구되고 지정되지 않은 인덱스의 기본 우선순위는 1입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;할당
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 속성 지정을 통한 인덱스 단위 샤드 할당 필터링 설정을 변경해 어떤 노드가 이 인덱스를 할당받을 수 있는지 변경합니다.&lt;/li&gt;
&lt;li&gt;인덱스 설정을 업데이트해 복제본 샤드의 개수도 변경 가능합니다.&lt;/li&gt;
&lt;li&gt;warm, cold에서 수행 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;migrate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index.routing.allocation.include._tier_preference 설정을 변경합니다.&lt;/li&gt;
&lt;li&gt;warm, colde 페이즈에서 수행 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스냅샷 대기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 삭제하기 전 지정한 SLM 정책을 통해 해당 인덱스에 대한 스냅샷 백업이 완료될 때까지 대기합니다.&lt;/li&gt;
&lt;li&gt;delete 페이즈에서 수행 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;삭제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 제거합니다.&lt;/li&gt;
&lt;li&gt;delete 페이즈에서 수행 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ILM 정책 생성과 적용&lt;/b&gt;&lt;br /&gt;정책은 키바나의 stack management 메뉴에서 index lifecycle policies 하위에서 create policy 버튼으로 생성할 수 있고 api로도 등록할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;PUT _ilm/policy/test-ilm-policy
{
  &quot;policy&quot;: {
    &quot;phases&quot;: {
      &quot;hot&quot;: {
        &quot;min_age&quot;: &quot;0ms&quot;,
        &quot;actions&quot;: {
          &quot;rollover&quot;: {
            &quot;max_primary_shard_size&quot;: &quot;4gb&quot;,
            &quot;max_age&quot;: &quot;1d&quot;
          }
        }
      },
      &quot;warm&quot;: {
        &quot;min_age&quot;: &quot;7d&quot;,
        &quot;actions&quot;: {
          &quot;forcemerge&quot;: {
            &quot;max_num_segments&quot;: 1
          },
          &quot;readonly&quot;: {}
        }
      },
      &quot;cold&quot;: {
        &quot;min_age&quot;: &quot;14d&quot;,
        &quot;actions&quot;: {
          &quot;migrate&quot;: {
            &quot;enabled&quot;: false
          },
          &quot;allocate&quot;: {
            &quot;number_of_replicas&quot;: 1
          }
        }
      },
      &quot;delete&quot;: {
        &quot;min_age&quot;: &quot;30d&quot;,
        &quot;actions&quot;: {
          &quot;wait_for_snapshot&quot;: {
            &quot;policy&quot;: &quot;my-dail-snapshot-policy&quot;
          },
          &quot;delete&quot; : {}
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;PUT [인덱스 이름]/_settings
{
  &quot;index.lifecycle.name&quot;: &quot;test-ilm-policy&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 직접 등록할 수 있는데 인덱스 템플릿을 이용하는 것이 편리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;슬로우 로그 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬로우 로그는 장애 원인을 추적하는데 도움이 되는데 기본적으로 설정이 저장되어 있지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;PUT _settings
{
  &quot;index.search.slowlog&quot;: {
    &quot;threshold&quot;: {
      &quot;query.warn&quot;: &quot;10s&quot;,
      &quot;query.info&quot;: &quot;5s&quot;,
      &quot;query.debug&quot;: &quot;2s&quot;,
      &quot;query.trace&quot;: &quot;500ms&quot;,
      &quot;fetch.warn&quot;: &quot;1s&quot;,
      &quot;fetch.info&quot;: &quot;800ms&quot;,
      &quot;fetch.debug&quot;: &quot;500ms&quot;,
      &quot;fetch.trace&quot;: &quot;200ms&quot;,
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로그는 샤드 레벨에서 측정됩니다. 검색 요청 전체에 소요된 시간을 측정해 로깅하는 것이 아닙니다. 느린 검색 로그를 남기도록 설정하려면 로그 디렉터리에 별도로 [클러스터 이름]_index_search_slow.log 파일이 생깁니다. 어떤 인덱스, 어떤 샤드에서 어떤 쿼리가 얼마나 시간을 소요했는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;PUT _settings
{
  &quot;index.indexing.slowlog&quot;: {
    &quot;source&quot;: &quot;1000&quot;,
    &quot;threshold&quot;: {
      &quot;index.warn&quot;: &quot;10s&quot;,
      &quot;index.info&quot;: &quot;5s&quot;,
      &quot;index.debug&quot;: &quot;2s&quot;,
      &quot;index.trace&quot;: &quot;500ms&quot;
    }
  }
}

PUT [인덱스 이름]/_settings&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;source 설정은 느린 색인 작업의 _source를 몇 글자까지 로깅할 것인지 지정합니다. treu로 지정하면 전체를 로깅하고 false나 0을 지정하면 로깅하지 않습니다. 느린 검색 로그와 마찬가지로 [클러스터 이름]_index_index_slowlog.log 파일이 생깁니다. 이는 인덱스 단위로도 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서킷 브레이커&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치는 처음부터 과도한 요청이 오는 경우 거부하는 정책을 채택했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필드 데이터 서킷 브레이커
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fielddata가 메모리에 올라갈 때 얼마만큼 메모리를 사용할지 예상합니다.&lt;/li&gt;
&lt;li&gt;기본값은 힙의 40%&lt;/li&gt;
&lt;li&gt;indices.breaker.fielddata.limit으로 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;요청 서킷 브레이커
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 하나의 데이터 구조가 메모리르 ㄹ과다하게 사용하는지 계산합니다.&lt;/li&gt;
&lt;li&gt;기본값은 힙의 60%&lt;/li&gt;
&lt;li&gt;indices.breaker.request.limit으로 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실행 중 요청(in-flight request) 서킷 브레이커
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드에 transport나 http를 통해 들어오는 모든 요청의 길이를 기반으로 메모리 사용량을 계산합니다.&lt;/li&gt;
&lt;li&gt;텍스트 원본 길이뿐만 아니라 요청 객체를 생성할 때 필요로 하는 메모리도 따집니다.&lt;/li&gt;
&lt;li&gt;기본값은 힙의 100%&lt;/li&gt;
&lt;li&gt;network.breaker.inflight_requests.limit으로 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;부모 서킷 브레이커
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 메모리의 실제 사용량을 기준으로 동작합니다.&lt;/li&gt;
&lt;li&gt;다른 자식 서킷 브레이커가 산정한 예상 메모리 사용량의 총합도 체크합니다.&lt;/li&gt;
&lt;li&gt;indices.breaker.total.use_real_memory 설정을 false로 변경하면 메모리의 실제 사용량은 체크하지 않습니다. 해당 값이 true인 경우 서킷 브레이커의 동작 기본값은 힙의 95%이고 false이면 70%입니다.&lt;/li&gt;
&lt;li&gt;indices.breaker.total.limit으로 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;PUT _cluster/settings
{
  &quot;transient&quot;: {
    &quot;indices&quot;: {
      &quot;breaker&quot;: {
        &quot;fielddata.limit&quot;: &quot;30%&quot;,
        &quot;request.limit&quot;: &quot;70%&quot;,
        &quot;total.limit&quot;: &quot;90%&quot;
      }
    },
    &quot;network&quot;: {
      &quot;breaker&quot;: {
        &quot;inflight_requests.limit&quot;: &quot;95%&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서킷 브레이커 설정은 클러스터 api를 이용해 동적으로 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클러스터 구성 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마스터 후보 노드와 데이터 노드 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 있는 서비스를 위해 클러스터를 구성하는 경우 마스터 후보 노드를 데이터 노드와 분리시켜야 합니다. 데이터 노드를 재시작할 필요가 없는데 재시작한다면 불필요한 샤드 복구 과정이 수행되고 생기는 부하가 장애 상황인 클러스터를 더 심각한 장애상황으로 만들 수 있습니다. 마스터 노드를 재시작할 필요가 없는데 재시작한다면 불필요한 마스터 재선출 과정이 발생할 수 있습니다. 따라서 서버를 매우 적게 써야 하는 상황이 아니라면 장애 상황을 대비해 마스터 후보 노드와 데이터 노드를 분리하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마스터 후보 노드 투표 구성원&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투표 구성원은 마스터 후보 노드의 부분 집합으로 일반적으로는 마스터 후보 노드와 동일한 집합입니다. 투표 구성원의 과반 이상의 결정으로 마스터 선출 등 중요한 의사가 결정됩니다. 마스터 후보 노드는 홀수대를 준비하는 것이 비용 대비 효용성이 좋습니다. 7 버전 미만의 경우 split brain 문제를 방지하기 위해 minimum_master_nodes 옵션을 과반으로 지정해야 했지만 7버전 이상부터는 해당 개념이 없고 split brain 문제가 원척적으로 일어나지 않는 구조입니다. 그러나 홀수대의 후보 노드를 준비하는 것이 더 비용 대비 효용성이 좋습니다. 엘라스틱서치가 투표 구성원을 홀수로 유지하기 위해 투표 구성원에서 마스터 후보 노드를 하나 빼두기 때문입니다. 이 한대는 투표 구성원에 참여하지 않고 투표 구성원 중 하나의 노드가 죽으면 빠져 있던 것이 대신 들어오는 구조인데 순차적으로 죽는 경우라면 1대의 실패를 더 견딜 수 있지만 동시에 죽으면 2K+1, 2k+2 구성이든 모두 k대의 동시 실패만 견디기 때문에 홀수대를 준비하는 것이 비용 대비 효용성이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 자원 대비 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최소 구성 3대
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모두 마스터 후보 &amp;amp; 마스터 겸임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사양이 낮은 4~5대 구성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3대는 마스터 후보와 데이터 역할 겸임&lt;/li&gt;
&lt;li&gt;2대는 데이터 노드 전용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사양이 높은 4~5대
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사양이 낮은 3대와 사양이 높은 3~4대 구성으로 변경&lt;/li&gt;
&lt;li&gt;사양이 낮은 장비에는 마스터 후보, 높은 장비에는 데이터 노드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;6~7대
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마스터 후보 노드와 데이터 노드를 완전히 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버 설정 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;힙크기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7.8 이상 7.11 미만 버전의 경우 config/jvm.options.d 디렉터리는 있지만 config/jvm.options 파일의 기본 Xms, Xmx 설정에 주석처리가 안 되어있습니다. 따라서 config/jvm.options 파일의 수치를 수정할지, config/jvm.options 파일의 Xmx와 Xmx 설정을 주석처리 해서 제거하고 config/jvm.option.d 디렉토리 밑에 새 파일을 생성해 설정할지 선택해야 합니다. 7.11 버전 이상의 경우 후자가 관리하기 편리합니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;vim config/jvm.options.d/heap-size.options
-Xms32736m
-Xmx32736m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙크기를 선택하는 데는 다음과 같은 원칙이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;루씬이 커널 시스템 캐시를 많이 활용하기 때문에 시스템 메모리의 절반은 운영체제가 캐시로 쓰도록 놔두는 것이 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;힙 크기를 32GB 이상 지정하지 않습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;32GB 이내의 힙 영역에만 접근한다면 Compressed OOPs 기능을 적용할 수 있기 때문인데 실제로 경곗값은 32GB보다 살짝 아래쪽입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;java -Xmx32736m -XX:+PrintFlagsFinal 2&amp;gt; /dev/null | grep UseCompressedOops

bool UseCompressedOops := true

java -Xmx32737m -XX:+PrintFlagsFinal 2&amp;gt; /dev/null | grep UseCompressedOops

bool UseCompressedOops := false &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;useCompressedOops 가 true가 되는 경곗값으로 힙 크기를 지정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compressed OOPs로 인코딩 된 주소를 실제 주소로 디코딩하려면 3비트 시프트 연산 후에 힙 영역이 시작되는 기본 주소를 더하는 작업이 필요한데 기본 주소를 0으로 바꿔준다면 시프트 연산을 제외한 나머지 과정을 없앨 수 있습니다. 이런 기능을 Zero-based compressed OOPs라고 합니다. 이를 적용하기 위한 힙 크기 경곗값은 Compressed OOPs보다 작으며 이도 확인해봐야 합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;java -XX:+UnlockDiagnosticVMOptions -Xlog:gc+heap+coops=debug -Xmx30721m -version

compressedOops mode : Non-zero disjoint base ...

java -XX:+UnlockDiagnosticVMOptions -Xlog:gc+heap+coops=debug -Xmx30720m -version

compressedOops mode : zero based ...&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;compressed Oops mode가 zero based로 적용되는 경곗값을 찾아야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스와핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치는 스와핑을 사용하지 않는 것을 권장합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 여기서 swap 부분 제거 
sudo vim /etc/fstab&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vm.max_map_count&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 프로세스가 최대 몇 개까지 메모리 맵 영역을 가질 수 있는지 지정합니다. 루씬은 mmap을 사용하므로 이 값을 높일 필요가 있습니다. 만약 262144보다 낮은 값이라면 높여야 합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;sudo vim /etc/sysctl.d/98-elasticsearch.conf

vm.max_map_count = 262144&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 기술자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치는 많은 file descriptor를 필요하여 최소 65535 이상으로 지정하도록 가이드합니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 확인
ulimit -a | grep &quot;open files&quot;

# 높이기
sudo vim /etc/security/limits.conf

# es 가동하는 유저명
username - nofile 65535&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Database</category>
      <category>ElasticSearch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/82</guid>
      <comments>https://backtony.tistory.com/82#entry82comment</comments>
      <pubDate>Sun, 18 Feb 2024 19:04:41 +0900</pubDate>
    </item>
    <item>
      <title>Elasticsearch - 집계 API</title>
      <link>https://backtony.tistory.com/81</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;집계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치의 집계는 검색의 연장선입니다. 집계의 대상을 추려낼 검색 조건을 검색 API에 담은 뒤 집계 조건을 추가해서 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;currency&quot;: {
        &quot;value&quot;: &quot;EUR&quot;
      }
    }
  },
  &quot;aggs&quot;: {
    &quot;my-agg&quot;: {
      &quot;sum&quot;: {
        &quot;field&quot;: &quot;taxless_total_price&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 요청 본문에 aggs를 추가하고 size는 0으로 지정했습니다. size를 0으로 지정하면 검색에 상위 매칭된 문서가 무엇인지 받아볼 수 없지만 검색 조건에 매치되는 모든 문서는 집계 작업 대상에 사용됩니다. 집계 작업은 검색 대상을 받아보는 용도가 아니기 때문에 size는 0으로 지정하여 점수를 계산하는 과정도 생략할 수 있습니다. 집계 요청의 상세는 aggs 밑에 기술하고 요청 한 번에 여러 집계를 요청할 수도 있기 때문에 결과에서 구분할 수 있도록 집계에 이름을 붙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메트릭 집계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;avg, max, min, sum&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;currency&quot;: {
        &quot;value&quot;: &quot;EUR&quot;
      }
    }
  },
  &quot;aggs&quot;: {
    &quot;my-agg&quot;: {
      &quot;sum&quot;: {
        &quot;field&quot;: &quot;taxless_total_price&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sum 자리에 avg, max, min으로 대체하여 똑같이 사용할 수 있습니다.&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;stats&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;currency&quot;: {
        &quot;value&quot;: &quot;EUR&quot;
      }
    }
  },
  &quot;aggs&quot;: {
    &quot;my-agg&quot;: {
      &quot;stats&quot;: {
        &quot;field&quot;: &quot;taxless_total_price&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stats 집계는 지정한 필드의 평균, 최댓값, 최솟값, 합, 개수를 모두 계산해서 반환합니다.&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;cardinality&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;currency&quot;: {
        &quot;value&quot;: &quot;EUR&quot;
      }
    }
  },
  &quot;aggs&quot;: {
    &quot;my-agg&quot;: {
      &quot;cardinality&quot;: {
        &quot;field&quot;: &quot;customer_id&quot;,
        &quot;precision_threshold&quot;: 3000
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정한 필드가 가진 고유한 값의 개수를 계산합니다. precision_threshold 옵션은 정확도를 조절하기 위해 사용합니다. 이 값을 높이면 정확도가 올라가지만 그만큼 메모리를 더 사용합니다. 해당 옵션을 무작정 높일 필요는 없고 precision_threshold가 최종 cardinality보다 높다면 정확도가 충분히 높기 때문에 적당한 값을 지정해 주는 것이 좋습니다. 기본값은 3000이며 최댓값은 40000입니다. cardinality가 높고 낮음과 관계없이 메모리 사용량은 precision_threshold에만 영향을 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버킷 집계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;range&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;GET kibana_sample_data_flights/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;my-range-agg&quot;: {
      &quot;range&quot;: {
        &quot;field&quot;: &quot;DistanceKilometers&quot;,
        &quot;ranges&quot;: [
          {
            &quot;to&quot;: 5000
          },
          {
            &quot;from&quot;: 5000,
            &quot;to&quot;: 10000
          },
          {
            &quot;from&quot;: 10000
          }
        ]
      },
      &quot;aggs&quot;: {
        &quot;my-price-agg&quot;:{
          &quot;avg&quot;: {
            &quot;field&quot;: &quot;AvgTicketPrice&quot;
          }
        }
      }
    }
  }
}

// response
{
  &quot;aggregations&quot; : {
    &quot;my-range-agg&quot; : {
      &quot;buckets&quot; : [
        {
          &quot;key&quot; : &quot;*-5000.0&quot;,
          &quot;to&quot; : 5000.0,
          &quot;doc_count&quot; : 4052,
          &quot;my-price-agg&quot; : {
            &quot;value&quot; : 513.3930266305937
          }
        },
        {
          &quot;key&quot; : &quot;5000.0-10000.0&quot;,
          &quot;from&quot; : 5000.0,
          &quot;to&quot; : 10000.0,
          &quot;doc_count&quot; : 6042,
          &quot;my-price-agg&quot; : {
            &quot;value&quot; : 677.2621444606182
          }
        },
        {
          &quot;key&quot; : &quot;10000.0-*&quot;,
          &quot;from&quot; : 10000.0,
          &quot;doc_count&quot; : 2965,
          &quot;my-price-agg&quot; : {
            &quot;value&quot; : 685.3553124773563
          }
        }
      ]
    }
  }
}&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;range 밑에 aggs를 하나 더 넣어서 하나의 집계에 대한 하위 집계를 추가할 수 있습니다. 하위 집계의 depth가 깊어지면 성능상 심각한 문제가 발생하므로 주의해야 합니다.&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;date_range&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;range 집계와 유사하나 date 타입 필드를 대상으로 한다는 점에서 from과 to에 간단한 날짜 시간 계산식을 사용할 수 있다는 점에 차이가 있습니다.&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;histogram&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_flights/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;my-histo&quot;: {
      &quot;histogram&quot;: {
        &quot;field&quot;: &quot;DistanceKilometers&quot;,
        &quot;interval&quot;: 1000
      }
    }
  }
}&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;지정한 필드의 값을 기준으로 버킷을 나눈다는 점에서 range 집계와 유사합니다. 다른 점은 버킷 구분의 경계 기준값을 직접 지정하는 것이 아니라 버킷의 간격을 지정해서 경계를 나눕니다. interval을 지정하면 필드의 최솟값과 최댓값을 확인한 후 그 사이를 interval에 지정한 간격을 쪼개서 버킷을 나눕니다. 특별히 지정하지 않으면 0을 기준으로 쪼개기를 시작하고 위치를 조정하고 싶다면 offset을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_flights/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;my-histo&quot;: {
      &quot;histogram&quot;: {
        &quot;field&quot;: &quot;DistanceKilometers&quot;,
        &quot;interval&quot;: 1000,
        &quot;offset&quot;: 500
      }
    }
  }
}&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;offset을 500으로 지정하면 [500,1500) 구간부터 시작할 것으로 예상할 수 있지만, interval이 1000이므로 [0,500) 사이의 구간은 [-500, 500) 구간이 추가되어 해당 구간에 포함됩니다. 이외에도 min_doc_count를 지정해서 버킷 내 문서 개수가 일정 이하인 버킷은 결과에서 제외할 수 있습니다.&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;date_histogram&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;histogram 집계와 유사하지만 대상으로 date 타입 필드를 사용한다는 점이 다릅니다. intervval 대신 calendar_interval이나 fixed_interval을 사용합니다.&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;terms&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_logs/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;my-terms&quot;: {
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;host.keyword&quot;,
        &quot;size&quot;: 10
      }
    }
  }
}&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;지정한 필드에 대해 가장 빈도수가 높은 term 순서대로 버킷을 생성합니다. 버킷을 최대 몇 개까지 생성할 것인지를 size로 지정합니다. terms 집계는 각 샤드에서 size 개수만큼 term를 뽑아서 빈도수를 셉니다. 각 샤드에서 수행된 계산을 한 곳으로 모아 합산한 후 size 개수만큼 버킷을 뽑습니다. 그러므로 size 개수와 각 문서의 분포에 따라 그 결과가 정확하지 않을 수 있습니다. 각 버킷의 doc_count는 물론 하위 집계 결과도 정확하지 않을 수 있고 특히 해당 필드의 고유한 term 개수가 size보다 많다면 상위에 뽑혀야 할 term이 최종 결과에 포함되지 않을 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 본문에 doc_count_error_upper_bound 필드는 doc_count의 오차 상한선을 나타냅니다. 이 값이 크다면 size를 높이는 것을 고려해야하는데 size를 높이면 정확도는 높아지지만 성능은 하락합니다. sum_other_doc_count 필드는 최종적으로 버킷에 포함되지 않은 문서수를 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모든 term에 대해서 페이지네이션으로 전부 순회하며 집계를 하려고 한다면 size를 높이는 것보다는 composite 집계를 사용하는 것이 좋습니다.&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;composite&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_logs/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;my-compos&quot;: {
      &quot;composite&quot;: {
        &quot;size&quot;: 100,
        &quot;sources&quot;: [
          {
            &quot;terms-aggs&quot;: {
              &quot;terms&quot;: {
                &quot;field&quot;: &quot;host.keyword&quot;
              }
            }
          },
          {
            &quot;date-his-agg&quot;:{
              &quot;date_histogram&quot;: {
                &quot;field&quot;: &quot;@timestamp&quot;,
                &quot;calendar_interval&quot;: &quot;day&quot;
              }
            }
          }
        ]
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sources로 지정한 하위 집계의 버킷 전부를 페이지네이션을 이용해 효율적으로 순회하는 집계입니다. sources에 하위 집계를 여러 개 지정한 뒤 조합된 버킷을 생성할 수 있습니다. composite 아래의 size는 페이지네이션 한 번에 몇 개의 버킷을 반환할 것인가를 지정합니다. sources에는 버킷을 조합하여 순회할 하위 집계를 지정합니다. 모든 종류의 집계를 하위 집계로 지정할 수는 없고 terms, histogram, date_histogram 등 일부 집계만 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, A 집계의 key가 1, 2이고, B 집계의 key가 a, b, c 라면 둘 조합은 다음과 같이 6개의 버킷으로 나뉘게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;A : 1, B : a
A : 1, B : b
A : 1, B : c
A : 2, B : a
A : 2, B : b
A : 2, B : c&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;첫 번째 검색 결과에는 after_key가 포함되서 응답으로 오는데 두 번째 검색부터는 after_key 내용을 추가하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_logs/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;my-compos&quot;: {
      &quot;composite&quot;: {
        &quot;size&quot;: 100,
        &quot;sources&quot;: [
          //..
        ],
        &quot;after&quot;: {
          &quot;terms-aggs&quot; : &quot;cdn.elastic-elastic-elastic.org&quot;,
          &quot;date-his-agg&quot; : 1710288000000
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파이프라인 집계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인 집계는 다른 집계 결과를 대상으로 집계 대상으로 합니다. 주로 buckets_path라는 인자를 통해 다른 집계의 결과를 가져오며 이는 상대 경로로 지정합니다. buckets_path는 다음을 구문을 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;gt; : 하위 집계로 이동하는 구분자&lt;/li&gt;
&lt;li&gt;. : 하위 메트릭으로 이동하는 구분자&lt;/li&gt;
&lt;li&gt;집계 이름&lt;/li&gt;
&lt;li&gt;메트릭 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;buckets_path는 하위 경로로 이동할수는 있지만 상위 경로로는 이동할 수 없습니다.&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;cumulative_sum&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 집계의 값을 누적하여 합산합니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;daily-timestamp&quot;: {
      &quot;date_histogram&quot;: {
        &quot;field&quot;: &quot;order_date&quot;,
        &quot;calendar_interval&quot;: &quot;day&quot;
      },
      &quot;aggs&quot;: {
        &quot;daily-total-quantity-average&quot;: {
          &quot;avg&quot;: {
            &quot;field&quot;: &quot;total_quantity&quot;
          }
        },
        &quot;pipeline-sum&quot;:{
          &quot;cumulative_sum&quot;: {
            &quot;buckets_path&quot;: &quot;daily-total-quantity-average&quot;
          }
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일 단위로 total_quantity의 평균을 구하기 위해 date_histogram으로 버킷을 나눈 뒤 그 하위 집계로 avg를 집계합니다. 그리고 date_histogram의 하위 집계로 cumulative_sum 집계를 추가했습니다. cumulative_sum은 buckets_path에서 누적 합산을 수행할 집계로 daily-total-quantity-average를 지정합니다. 이렇게 하면 cumulative_sum을 수행할 때마다 daily-total-quantity-average를 찾아서 그 합을 누적 합산합니다.&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;max_bucket&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 0,
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  },
  &quot;aggs&quot;: {
    &quot;daily-timestamp&quot;: {
      &quot;date_histogram&quot;: {
        &quot;field&quot;: &quot;order_date&quot;,
        &quot;calendar_interval&quot;: &quot;day&quot;
      },
      &quot;aggs&quot;: {
        &quot;daily-total-quantity-average&quot;: {
          &quot;avg&quot;: {
            &quot;field&quot;: &quot;total_quantity&quot;
          }
        }
      }
    },
    &quot;max-total-quantity&quot;:{
      &quot;max_bucket&quot;: {
        &quot;buckets_path&quot;: &quot;daily-timestamp&amp;gt;daily-total-quantity-average&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 집계의 결과를 받아서 그 결과가 가장 큰 버킷의 key와 결괏값을 구합니다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>ElasticSearch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/81</guid>
      <comments>https://backtony.tistory.com/81#entry81comment</comments>
      <pubDate>Wed, 14 Feb 2024 22:51:28 +0900</pubDate>
    </item>
    <item>
      <title>Elasticsearch -  검색 API</title>
      <link>https://backtony.tistory.com/80</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;단건 문서 API&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;색인&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;PUT [인덱스 이름]/_doc/[_id값]  // 가장 기본
POST [인덱스 이름]/_doc
PUT [인덱스 이름]/_create/[_id값]
POST [인덱스 이름]/_create/[_id값]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PUT의 경우 요청 본문에 담아 보낸 JSOPN 문서를 지정된 인덱스와 _id값으로 색인을 시도합니다. 만약 같은 _id값을 가진 문서가 있다면 덮어씌웁니다. 반면에 POST 메서드는 _id값을 지정하지 않고 색인을 요청할 경우에 사용합니다. 이 경우에는 랜덤한 _id값이 지정됩니다. _create이 경로에 들어가는 API는 항상 새로운 문서 생성만을 허용하고 덮어씌우면서 색인하는 것은 금지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조회&lt;/h3&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_doc/[_id값]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 API는 문서 단건을 조회합니다. 이는 검색과는 다르게 refresh되지 않은 상태에서도 변경된 내용을 확인할 수 있습니다. 애초에 고유한 식별자를 지정해서 단건 문서를 조회하는 것은 역색인을 사용할 필요가 없기 때문에 translog에서 읽어옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필드 필터링&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스]/_doc/[_id값]?_source_includes=p*,views
GET [인덱스]/_doc/[_id값]?_source_includes=p*,views&amp;amp;_source_excludes=public&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 API 사용 시 _source_includes와 _source_excludes 옵션을 사용하면 결과에 원하는 필드만 필터링해서 포함시킬 수 있습니다. 전자는 포함시키는 것이고 후자는 제외시키는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;업데이트 API&lt;/h3&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;POST [인덱스 이름]/_update/[_id값]
{
  &quot;doc&quot;: {
    [업데이트할 내용]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루씬의 세그먼트는 불변이기 때문에 업데이트 시 기존 문서를 수정하는 것이 아니라 기존 문서의 내용을 조회한 뒤 부분 업데이트될 내용을 합쳐 새 문서를 만들어 색인하는 형태로 진행됩니다. 그리고 현재 문서와 동일한 내용이라 업데이트할 것이 없다면 검색 결과 result 필드에 noop이라는 응답이 나옵니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;POST [인덱스 이름]/_update/[_id값]
{
  &quot;doc&quot;: {
    [업데이트할 내용]
  },
  &quot;doc_as_upsert&quot;: true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 API는 기존 문서를 조회한 뒤 업데이트 시키기 때문에 기존 문서가 없다면 실패합니다. 기존 문서가 없을 때에는 새로 문서를 추가하는 upsert가 필요하다면 옵션을 켜야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제 API&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;DELETE [인덱스 이름] // 인덱스 전체 삭제
DELETE [인덱스 이름]/_doc/[_id값] // 특정 문서 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;복수 문서 API&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;bulk API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bulk API는 여러 색인, 업데이트, 삭제 작업을 한 번의 요청에 담아서 보내는 API입니다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;POST [인덱스 이름]/_bulk&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST _bulk
{&quot;index&quot;: {&quot;_index&quot;: &quot;bulk_test&quot;,&quot;_id&quot;:&quot;1&quot;}}
{&quot;field&quot;: &quot;value1&quot;}
{&quot;delete&quot;: {&quot;_index&quot;: &quot;bulk_test&quot;,&quot;_id&quot;:&quot;2&quot;}}
{&quot;create&quot;: {&quot;_index&quot;: &quot;bulk_test&quot;,&quot;_id&quot;:&quot;1&quot;}}
{&quot;field&quot;: &quot;value1&quot;}
{&quot;update&quot;: {&quot;_id&quot;:&quot;1&quot;,&quot;_index&quot;: &quot;bulk_test&quot;}}
{&quot;field&quot;: &quot;value1&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index, create 요청은 색인 요청인데 index는 색인, 덮어쓰기가 가능하고 create는 새 문서를 생성하는 것만 허용하고 덮어쓰지는 않습니다. _bulk API의 경우 인덱스 이름을 명시하지 않은 경우, 요청 body에 index이름을 따라가고, 인덱스 이름을 명시하고 요청 body에 index이름을 명시하지 않는다면 인덱스 이름을 따라갑니다. 응답은 각 요청을 수행하고 난 결과를 모아 하나의 응답으로 돌아옵니다. 응답으로 나온 결과의 순서가 bulk API의 실행 순서를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bulk API의 기술된 작업이 반드시 그 순서대로 수행된다고 보장되진 않습니다. 조정 역할을 하는 노드가 요청을 수신하면 각 요청의 내용을 보고 적절한 주 샤드로 요청을 넘겨주고 여러 개의 주 샤드에 넘어간 각 요청은 각자 독립적으로 수행되기 때문에 요청 간 순서는 보장되지 않습니다. 하지만 완전히 동일한 인덱스, _id, 라우팅 조합을 가진 요청은 반드시 동일한 주 샤드로 넘어갑니다. 이 경우에는 한 bulk API 내에서 이 조합이 같은 요청에 대해서는 bulk API가 기술된 순서대로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;multi get API&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;GET _mget
GET [인덱스 이름]/_mget

// ex
GET bulk_test/_mget
{
  &quot;ids&quot;: [&quot;1&quot;, &quot;2&quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검색 API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색에 대해 설명하기 앞서, 엘라스틱서치에서 와일드카드 검색을 사용하지 않는 것이 권장됩니다. 사용해야 한다면 반드시 매핑 설정과 데이터 양상을 파악한 뒤 그 여파를 정확히 판단할 수 있는 상태에서만 제한적으로 사용해야 합니다. 그중에서도 *ello, ?ello 같이 와일드카드 문자가 앞에 오는 쿼리는 더욱 주의해야 합니다. 인덱스가 들고 있는 모든 term을 가지고 검색을 돌려서 확인하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색 대상 지정과 쿼리 DSL 검색&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
POST [인덱스 이름]/_search
GET _search
POST _search

// ex
GET my_test*,mapping_test/_search&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;GET, POST 둘 다 동작은 동일하며 인덱스 이름을 지정하지 않으면 전체 인덱스에 대해 검색합니다. 인덱스 이름을 지정할 때는 와일드카드 문자를 사용할 수 있고 콤마로 구분하여 검색 대상을 여럿 지정하는 것도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 쿼리를 지정하는 방법은 요청 본문에 엘라스틱서치 전용 쿼리 DSL을 명시하는 방법과 요청 주소줄에 q라는 매개변수를 넣고 루씬 쿼리 문자열을 지정해 검색하는 방법이 있습니다. 쿼리가 매우 단순한 경우가 아니라면 후자는 거의 사용하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET my_test/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;hello&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;match_all&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: { }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;match_all 쿼리는 모든 문서를 매치하는 쿼리입니다. query 부분을 비워두면 기본값으로 지정되는 쿼리입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;match&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;hello world&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;match는 지정한 필드의 내용과 일치하는 문서를 찾습니다. 필드가 text 타입이라면 애널라이저로 분석됩니다. 위에서는 hello, world 두 토큰으로 분석되어 2개의 텀을 찾아 매치되는 문서를 반환합니다. 이때 match 쿼리는 OR 조건으로 동작합니다. 만약 and 조건으로 변경하고 싶다면 operator를 추가해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: {
        &quot;query&quot;: &quot;hello world&quot;,
        &quot;operator&quot;: &quot;and&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;term&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;fieldName&quot;: {
        &quot;value&quot;: &quot;VALUE&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;term 쿼리는 지정한 필드 값이 정확히 일치하는 문서를 찾습니다. 대상 필드에 노멀라이저가 지정돼 있다면 질의어도 노멀라이저가 처리됩니다. term 쿼리는 대상이 text타입인 경우, 질의어는 노멀라이저 처리를 거치지만 필드의 값은 애널라이저로 분석한 뒤 생성된 역색인을 이용합니다. 즉, 필드 값이 애널라이저로 분석되어 역색인 된 토큰들과 노멀라이저 처리된 질의어가 완전히 일치하는 경우에만 검색에 걸립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;terms&lt;/h3&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;terms&quot;: {
      &quot;fieldName&quot;: [&quot;hello&quot;, &quot;world&quot;]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;term 쿼리와 유사하지만 질의어를 여러 개 지정할 수 있습니다. 하나 이상의 질의어가 일치하면 검색 결과에 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;range 쿼리&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;fieldName&quot;: {
        &quot;gte&quot;: 100,
        &quot;lt&quot;: 200
      }
    }
  }
}&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;지정한 필드의 값이 특정 범위 내에 있는 문서를 찾습니다. 범위는 gt, lt, gte, lte를 사용할 수 있습니다. 엘라스틱서치는 문자열 필드를 대상으로 한 range 쿼리는 부하가 큰 쿼리로 분류하므로 주의해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;dateField&quot;: {
        &quot;gte&quot;: &quot;2023-02-15T11:11:11.000Z||+36h/d&quot;,
        &quot;lte&quot;: &quot;now-3h/d&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;now : 현재 시각&lt;/li&gt;
&lt;li&gt;|| : 날짜 시간 문자열의 마지막에 붙이고 이 뒤에 붙는 문자열은 시간 계산식으로 파싱 됩니다.&lt;/li&gt;
&lt;li&gt;+와 - : 지정된 시간만큼 더하거나 빼는 연산을 수행합니다.&lt;/li&gt;
&lt;li&gt;/ : 버림을 수행합니다. 예를 들어 /d는 날짜 단위 이하의 시간을 버림 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;날짜 시간 단위&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;height: 172px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;기호&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;단위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;y&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;연도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;M&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;w&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;주&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;d&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;날짜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;h&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;H&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;m&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;s&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;prefix&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;prefix&quot;: {
      &quot;fieldName&quot;: {
        &quot;value&quot;: &quot;hello&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드값이 지정한 질의어로 시작하는 문서를 찾는 쿼리로 무거운 쿼리로 분류됩니다. 와일드카드 쿼리만큼은 아니지만 단발성 쿼리 정도는 감수할 만한 성능이 나옵니다. 하지만 일반적인 서비스성 쿼리로는 적절하지 못합니다. 만약 서비스용으로 사용해야 한다면 매핑 시에 index_prefixes를 설정에 넣는 방법을 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;PUT prefix
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;fieldName&quot;: {
        &quot;type&quot;: &quot;text&quot;,
        &quot;index_prefixes&quot;:{
          &quot;min_chars&quot;: 3,
          &quot;max_chars&quot;: 5
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매핑 시에 index_prefixes를 지정하면 색인할 때, min, max_chars 사이의 prefix를 미리 별도 색인을 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;exists&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;PUT [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;exists&quot;: {
      &quot;field&quot;: &quot;fieldName&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정한 필드를 포함한 문서가 있는지 검색합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;bool 쿼리&lt;/h3&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;PUT [인덱스 이름]/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {&quot;term&quot;: {
          &quot;title&quot;: {
            &quot;value&quot;: &quot;hello&quot;
          }
        }},
        {&quot;term&quot;: {
          &quot;body&quot;: {
            &quot;value&quot;: &quot;world&quot;
          }
        }}
      ],
      &quot;must_not&quot;: [
        {&quot;term&quot;: {
          &quot;message&quot;: {
            &quot;value&quot;: &quot;backtony&quot;
          }
        }}
      ],
      &quot;filter&quot;: [
        {&quot;term&quot;: {
          &quot;message&quot;: &quot;terrapy&quot;
        }}
      ],
      &quot;should&quot;: [
        {&quot;match&quot;: {
          &quot;field2&quot;: &quot;world&quot;
        }}
      ],
      &quot;minimum_should_match&quot;: 1
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 쿼리를 조합하여 검색합니다. must, must_not, filter, should 4가지 종류의 조건절에 다른 쿼리를 조합하여 사용합니다. must 조건절과 filter 조건절에 들어간 하위 쿼리는 모두 AND 조건으로 만족해야 최종 검색 결과에 포함됩니다. must_not 조건절에 들어간 쿼리를 만족하는 문서는 제외되며 should 조건절에 들어간 쿼리는 minimum_should_match에 지정한 개수 이상의 하위 쿼리를 만족하는 문서가 결과에 포함됩니다. minimum_should_match의 기본값은 1이며 이 값이 1이라면 should 조건절은 or 조건으로 검색하는 것과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bool 쿼리는 여러 쿼리를 조합하게 되는데 어떤 쿼리가 먼저 수행되는지에 대한 규칙은 없습니다. 엘라스틱서치가 쿼리를 받으면 내부적으로 쿼리를 재작성하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿼리 문맥과 필터 문맥&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참과 거짓으로 따지는 검색 과정을 필터 문맥이라고 하고, 문서가 주어진 검색 조건을 얼마나 더 만족하는지 유사도 점수를 매기는 검색 과정을 쿼리 문맥이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;th&gt;쿼리 문맥&lt;/th&gt;
&lt;th&gt;필터 문맥&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;질의 개념&lt;/td&gt;
&lt;td&gt;얼마나 잘 매치되는지&lt;/td&gt;
&lt;td&gt;조건을 만족하는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;점수&lt;/td&gt;
&lt;td&gt;계산 O&lt;/td&gt;
&lt;td&gt;계산 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능&lt;/td&gt;
&lt;td&gt;상대적 느림&lt;/td&gt;
&lt;td&gt;상대적 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;캐시&lt;/td&gt;
&lt;td&gt;쿼리 캐시 X&lt;/td&gt;
&lt;td&gt;쿼리 캐시 O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;종류&lt;/td&gt;
&lt;td&gt;bool의 must, should 또는 match, term 등&lt;/td&gt;
&lt;td&gt;bool의 filter, must_not 또는 exists, range, constant_score 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search?routing=[라우팅]
{
  &quot;query&quot;: {
    // ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색, 색인, 조회 API 모두 제대로 된 라우팅을 지정하는 것이 좋습니다. 라우팅을 지정하지 않으면 전체 샤드에 검색 요청이 들어갑니다. 반면 라우팅을 지정하면 정확히 한 샤드에만 검색 요청이 들어가므로 성능상 이득이 매우 큽니다. 물론 비즈니스 특성상 전체 샤드를 대상으로 검색을 수행해야 한다면 라우팅을 지정하지 않아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정렬&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;GET [인덱스 이름]/_search
{
  &quot;query&quot;: {
    // ...
  },
  &quot;sort&quot;: [
    {&quot;field1&quot;:  { &quot;order&quot;: &quot;desc&quot;}},
    {&quot;field2&quot;:  { &quot;order&quot;: &quot;asc&quot;}},
    &quot;field3&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sort를 지정해서 검색 결과를 정렬할 수 있습니다. 정렬 대상 필드를 여럿 지정할 수 있고 fields 같이 이름만 명시하면 내림차순으로 정렬됩니다. 정렬 대상 필드의 타입으로 숫자, date, boolean, keyword 타입은 가능하지만 text 타입은 대상으로 지정할 수 없습니다. 필드 이름 외에도 _score(유사도)나 _doc(문서 번호 순서)를 지정할 수 있습니다. sort 옵션을 지정하지 않으면 기본은 _score 내림차순이고, 정렬 옵션에 _score가 포함되지 않으면 유사도를 계산하지 않습니다. 만약 어떤 순서로 정렬되어도 상관없는 경우라면 _doc 단독 정렬을 사용하면 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지네이션&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;from과 size&lt;/h4&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;PUT [인덱스 이름]/_search
{
  &quot;from&quot;: 10,
  &quot;size&quot;:5,
  &quot;query&quot;: {
    // ..
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;size는 몇 개의 문서를 반환할지, from은 몇 번째 문서부터 결과를 반환할지를 의미합니다. 위의 경우 유사도 점수를 내림차순 정렬된 문서들 중, 11번째부터 15번째 까지 5개의 문서가 반환됩니다. from의 기본값은 0, size는 10입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;from과 size는 제한적으로 사용해야 하며 본격적인 페이지네이션에는 사용할 수 없습니다. 만약 다음 페이지의 결과를 가져오기 위해 from 15, size 5로 지정해 검색을 요청했다고 한다면 내부적으로는 20개의 문서를 수집하는 검색을 수행한 뒤 마지막의 결과 일부를 반환하는 방식으로 동작합니다. 이런 동작과정 때문에 2가지 문제가 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;from의 값이 올라갈수록 매우 무거운 검색을 수행하게 된다.&lt;/li&gt;
&lt;li&gt;이전 페이지를 검색할 때의 상태와 페이지를 넘기고 다음 검색을 수행할 때의 인덱스 상태가 동일하지 않다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 검색 요청 사이에 새로운 문서가 색인되거나 삭제될 수도 있기 때문에 특정 시점의 데이터를 중복이나 누락 없이 엄밀하게 페이지네이션을 제공해야 한다면 from과 size는 사용하지 말아야 합니다. 첫 번째 문제 때문에 엘라스틱서치는 from과 size를 조합해 검색할 수 있는 최대 윈도우 크기를 from과 size의 합을 1만 개로 제한하고 있습니다. 인덱스를 늘릴 수는 있지만 이 방법은 권하지 않고 있으며 scroll이나 search_after 방법을 사용해야 합니다.&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;scroll&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scroll은 검색 조건에 매핑되는 전체 문서를 모두 순회해야 할 때 적합한 방법입니다. 스크롤을 순회하는 동안에는 최초 검색 시의 문맥(search context)이 유지됩니다. 중복이나 누락도 발생하지 않습니다. scroll 매개변수로 검색 문맥을 유지할 시간을 지정해서 검색할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// 첫 검색
PUT [인덱스 이름]/_search?scroll=1m
{
  &quot;size&quot;: 1000,
  &quot;query&quot;: {
    // ..
  }
}

// response
{
  &quot;_scroll_id&quot;: &quot;abcde&quot;
}

// 두번째 검색
GET _search/scroll
{
  &quot;scroll_id&quot;: &quot;abcde&quot;        
  &quot;scroll&quot;: &quot;1m&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 검색의 response로 scroll_id가 반환되고 이후부터는 scroll_id로 검색하면 더 이상 문서가 반환되지 않을 때까지, scroll 검색을 반복할 수 있습니다. scroll 검색을 한 번 수행할 때마다 검색 문맥이 연장됩니다. 즉, scroll 매개변수에 지정한 검색 문맥의 유지 시간은 배치와 배치 사이를 유지할 정도의 시간으로 지정하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scroll은 검색 문맥을 보존한 뒤 전체 문서를 순회하는 동작 특성상 검색 결과의 정렬 여부가 상관없는 작업에 사용하는 경우가 많습니다. 이 경우에는 _doc로 정렬을 지정하는 것이 좋습니다. 이렇게 지정하면 유사도 점수를 계산하지 않으며 정렬을 위한 별도의 자원도 사용하지 않습니다. 이를 통해 scroll 성능을 끌어올릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;// 첫 검색
PUT [인덱스 이름]/_search?scroll=1m
{
  &quot;size&quot;: 1000,
  &quot;query&quot;: {
    // ..
  },
  &quot;sort&quot;: [
    &quot;_doc&quot;    
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scroll은 서비스에서 지속적 호출을 의도하고 만들어진 기능이 아닙니다. 주로 대량의 데이터를 다른 스토리지로 이전하거나 덤프하는 용도로 만들어졌기 때문에 서비스에서 사용자가 지속적으로 호출하기 위한 용도는 아닙니다.&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;search_after&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에서 사용자가 검색 결과를 페이지네이션으로 제공하는 용도의 경우라면 search_after이 가장 적합합니다. search_after에는 sort를 지정해야 합니다. 이때 동일한 정렬 값이 등장할 수 없도록 최소한 1개 이상의 동점 제거(tiebreaker: 동일 스코어를 가진 문서들 사이에 어떤 문서를 먼저 반환할지)용 필드를 지정해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 20,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;currency&quot;: &quot;EUR&quot;
    }
  },
  &quot;sort&quot;: [
    {
      &quot;order_date&quot;: &quot;desc&quot;,
      &quot;order_id&quot;: &quot;asc&quot;
    }
  ]
}

// response
{
  // ...
    &quot;hits&quot; : [
      {
        &quot;sort&quot; : [
          1709411990000,
          &quot;591924&quot;
        ]
      }
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 검색이 끝나면 결과의 가장 마지막 문서에 표시된 sort 기준값을 가져와 search_after 부분에 넣어 그다음 검색을 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;GET kibana_sample_data_ecommerce/_search
{
  &quot;size&quot;: 20,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;currency&quot;: &quot;EUR&quot;
    }
  },
  &quot;search_after&quot;: [1709411990000, &quot;591924&quot;],
  &quot;sort&quot;: [
    {
      &quot;order_date&quot;: &quot;desc&quot;,
      &quot;order_id&quot;: &quot;asc&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동점 제거용 필드(tiebreaker)는 스코어가 동일한 경우 어떤 문서를 먼저 반환할 것인지를 정하는 것이기 때문에 문서를 고유하게 특정할 수 있는 값이 들어가야 합니다. 그러나 _id값을 동점 제거용 기준 필드로 사용하는 것은 좋지 않습니다. _id 필드는 doc_values가 꺼져있기 때문에 이를 기준으로 하는 정렬은 많은 메모리를 사용하게 됩니다. 따라서 _id 필드값과 동일한 값을 별도의 필드에 저장해 뒀다가 동점 제거용으로 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 동점 제거용 필드를 제대로 지정했다 하더라도 인덱스 상태가 변하는 도중이라면 페이지네이션 과정에서 누락되는 문서가 발생하는 등 일관적이지 않은 변동 사항이 발생할 수 있습니다. search_after를 사용할 때 인덱스 상태를 특정 시점으로 고정하려면 point in time API를 조합해서 사용해야 합니다.&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;point in time API&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;point in time API는 검색 대상의 상태를 고정할 때 사용합니다. keep_alive 매개변수에 상태를 유지할 시간을 지정합니다. 해당 기능은 7.10 버전 이후부터 사용할 수 있으며, oss버전에서는 지원되지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;POST /kibana_sample_data_ecommerce/_pit?keep_alive=1m

// response
{
  &quot;id&quot;: &quot;abcd&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 얻은 id값을 search_after에서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;GET _search
{
  &quot;size&quot;: 20,
  &quot;query&quot;: {
    // ..
  },
  &quot;pit&quot;: {
    &quot;id&quot;: &quot;abcd&quot;,
    &quot;keep_alive&quot;: &quot;1m&quot;
  },
  &quot;sort&quot;: [
    {
      &quot;order_date&quot;: &quot;desc&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pit 부분에 얻어온 pit id를 지정해서 사용하고, 검색 대상이 될 인덱스는 지정하지 않습니다. pit를 지정하는 것 자체가 검색 대상을 지정하는 것이기 때문입니다. pit를 지정하면 동점 제거용 필드를 별도로 지정할 필요가 없습니다. 정렬 기준 필드를 하나라도 지정했다면 _shard_doc이라는 동점 제거용 필드에 대한 오름차순 정렬이 맨 마지막에 자동으로 추가됩니다. 다만 정렬 기준을 아예 지정하지 않으면 정렬 기준으로 _shard_doc이 추가되지 않습니다. 따라서 search_after에서 조합해서 사용하고 싶은 경우 정렬 기준 필드를 최소한 하나는 지정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// 다음 검색
GET _search
{
  &quot;size&quot;: 20,
  &quot;query&quot;: {
    // ..
  },
  &quot;pit&quot;: {
    &quot;id&quot;: &quot;abcd&quot;,
    &quot;keep_alive&quot;: &quot;1m&quot;
  },
  &quot;search_after&quot;: [1709411990000, &quot;591924&quot;],
  &quot;sort&quot;: [
    {
      &quot;order_date&quot;: &quot;desc&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Data Elasticsearch&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/database/es/%EA%B2%80%EC%83%89/spring-data-elasticsearch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃허브&lt;/a&gt; 에 간단한 데모를 작성해두었습니다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>ElasticSearch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/80</guid>
      <comments>https://backtony.tistory.com/80#entry80comment</comments>
      <pubDate>Tue, 13 Feb 2024 23:44:45 +0900</pubDate>
    </item>
    <item>
      <title>Elasticsearch - 기본 콘셉트와 구조도</title>
      <link>https://backtony.tistory.com/79</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;ElasticSearch 기본 콘셉트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루씬은 데이터를 색인하고 검색하는 기능을 제공하는 검색 엔진의 코어 라이브러리입니다. 엘라스틱서치는 루씬을 코어로 이용하여 JSON 문서의 저장, 색인, 검색 등의 작업을 분산 처리하는 검색 엔진입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색 엔진&lt;/b&gt;&lt;br /&gt;단순한 텍스트 매칭 검색이 아닌 전문 검색이 가능하며 다양한 종류의 검색 쿼리를 지원합니다. 검색 엔진이기 때문에 역색인을 사용하여 검색 속도도 매우 빠르며, 다양한 애널라이저를 조합하여 요구사항에 맞는 색인을 구성할 수 있고 형태소 분석도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분산 처리&lt;/b&gt;&lt;br /&gt;데이터를 여러 노드에 분산 저장하여 검색이나 집계 작업 등을 수행할 때도 분산 처리를 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고가용성 제공&lt;/b&gt;&lt;br /&gt;클러스터를 구성하고 있는 일부 노드에 장애가 발생해도 복제본 데이터를 이용해 중단 없이 서비스를 지속할 수 있습니다. 이때 엘라스틱서치는 다시 복제본을 만들어 복제본의 개수를 유지하면서 노드 간 데이터의 균형을 자동으로 맞춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수평적 확장성&lt;/b&gt;&lt;br /&gt;더 많은 처리가 필요할 때 새로운 노드에 엘라스틱서치를 설치하여 클러스터에 참여시키는 것만으로도 확장이 가능합니다. 새 노드에 데이터를 복제하거나 옮기는 작업도 엘라스틱서치가 자동으로 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JSON 기반의 REST API 제공&lt;/b&gt;&lt;br /&gt;JSON 형태의 문서를 저장, 색인, 검색하며, 작업 요청을 보낼 때도 JSON 기반의 REST API를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 안정성&lt;/b&gt;&lt;br /&gt;데이터 색인 요청 후 200 OK를 받았다면 데이터는 확실히 디스크에 기록됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다양한 플러그인을 통한 기능 확장 지원&lt;/b&gt;&lt;br /&gt;공식적으로 지원하는 플러그인도 많고 커뮤니티에서 다양한 서드파티 플러그인을 공개하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;준실시간 검색&lt;/b&gt;&lt;br /&gt;데이터를 색인하자마자 조회하는 것은 가능하지만, 데이터 색인 직후의 검색 요청은 성공하지 못할 가능성이 높습니다. 역색인을 구성하고 역색인으로부터 검색이 가능해지기까지 시간이 걸리기 때문입니다. 기본 설정으로 운영할 경우 최대 1초 정도 시간이 걸립니다.&lt;/p&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;트랜잭션 지원 X&lt;/b&gt;&lt;br /&gt;RDBMS와 달리 트랜잭션을 지원하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조인을 지원하지 않음&lt;/b&gt;&lt;br /&gt;join이라는 특별한 데이터 타입이 있지만 제한적인 상황을 위한 기능이며 성능도 떨어집니다. RDBMS와 다르게 데이터를 비정규화해야 하며 설계에 있어서는 조인을 사용하지 않는다고 생각해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ELK stack&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPhlGI/btsEFTOMLvq/I1BG6uikkMfaYRZIyOy3x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPhlGI/btsEFTOMLvq/I1BG6uikkMfaYRZIyOy3x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPhlGI/btsEFTOMLvq/I1BG6uikkMfaYRZIyOy3x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPhlGI%2FbtsEFTOMLvq%2FI1BG6uikkMfaYRZIyOy3x1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;414&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치는 단독으로 검색을 위해 사용하거나 ELK(Elasticsearch &amp;amp; Logstash &amp;amp; Kibana) 스택을 기반으로 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Filebeat : 로그를 생성하는 서버에 설치래 로그를 수집하여 Logstash 서버로 로그를 전송합니다.&lt;/li&gt;
&lt;li&gt;Logstash : 로그 및 트랜잭션 데이터를 수집하여 집계 및 파싱(정제 및 전처리)을 하고 ES로 전달합니다.&lt;/li&gt;
&lt;li&gt;Elasticsearch : Logstash로부터 전달받은 데이터를 저장하고, 검색 및 집계 등의 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;Kibana : 저장된 로그를 Elasticsearch의 빠른 검색을 통해 가져와 시각화 및 모니터링 기능을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엘라스틱서치 내부 구조와 루씬&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루씬은 문서를 색인하고 검색하는 라이브러리로 엘라스틱서치는 아파치 루씬을 코어 라이브러리로 사용합니다.&lt;/p&gt;
&lt;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;루씬 flush&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnW9QG/btsEHHfK9Kh/WN9tV0JeI5qVeykOzRT7uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnW9QG/btsEHHfK9Kh/WN9tV0JeI5qVeykOzRT7uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnW9QG/btsEHHfK9Kh/WN9tV0JeI5qVeykOzRT7uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnW9QG%2FbtsEHHfK9Kh%2FWN9tV0JeI5qVeykOzRT7uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1258&quot; height=&quot;362&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 색인 요청이 들어오면 루씬은 문서를 분석해서 역색인을 생성합니다. 최초 생성 자체는 메모리 버퍼에 들어갑니다. 문서 색인, 업데이트, 삭제 등의 작업이 수행되면 루씬은 변경들을 메모리에 들고 있다가 주기적으로 디스크에 flush 합니다. 루씬은 색인한 정보를 파일로 저장하기 때문에 루씬에서 검색을 하려면 먼저 파일을 열어야 합니다. 루씬은 파일을 연 시점에 색인이 완료된 문서만 검색할 수 있습니다. 이후 색인에 변경사항이 발생했고, 그 내용을 검색 결과에 반영하고 싶다면 파일을 새로 열어야 합니다. 이러한 작업을 엘라스틱서치에서는 refresh라고 합니다. refresh는 어느 정도 비용이 있는 작업이기 때문에 엘라스틱서치는 색인이 변경될 때마다 refresh를 수행하지 않고 적절한 간격마다 주기적으로 실행합니다.&lt;/p&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;루씬 commit&lt;/b&gt;&lt;br /&gt;루씬의 flush는 시스템의 페이지 캐시에 데이터를 넘겨주는 것까지만 보장할 뿐 디스크에 파일이 실제로 안전하게 기록되는 것까지 보장하지는 않습니다. 따라서 루씬은 fsync 시스템 콜을 통해 주기적으로 커널 시스템의 페이지 캐시의 내용과 실제로 디스크에 기록된 내용의 싱크를 맞추는 작업을 수행합니다. 이를 루씬 commit이라고 합니다. 엘라스틱서치의 flush 작업은 내부적으로 루씬 commit을 거칩니다. 루씬의 flush와 엘라스틱서치의 flush는 다른 개념입니다. 엘라스틱서치의 flush는 엘라스틱서치 refresh보다도 훨씬 비용이 더 드는 작업입니다. 따라서 refresh와 마찬가지로 적절한 주기로 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세그먼트&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zfEVe/btsEIOZ6DeO/gAxaLTnUtNfFcFY3Byp4jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zfEVe/btsEIOZ6DeO/gAxaLTnUtNfFcFY3Byp4jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zfEVe/btsEIOZ6DeO/gAxaLTnUtNfFcFY3Byp4jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzfEVe%2FbtsEIOZ6DeO%2FgAxaLTnUtNfFcFY3Byp4jK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1270&quot; height=&quot;368&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 작업을 거쳐 디스크에 기록된 파일들이 모이면 세크먼트라는 단위가 됩니다. 루씬 검색은 모든 세그먼트를 대상으로 수행합니다. 세그먼트 자체는 불변인 데이터로 구성되며 새로운 문서가 들어오면 새로운 세그먼트가 생성됩니다. 기존 문서를 삭제하는 경우 삭제 플래그만 표시하며 기존 문서가 업데이트 되는 경우도 삭제 플래그를 추가하고 세그먼트를 생성합니다. 불변인 세그먼트 개수를 계속 늘릴 수는 없기 때문에 루씬은 적당히 세그먼트의 병합을 수행하고 병합이 수행될 때 삭제 플래그가 표시된 데이터를 실제로 삭제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;루씬 인덱스와 엘라스틱서치 인덱스&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2418&quot; data-origin-height=&quot;1188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUEVrw/btsEKGgpVJM/rWU99LyvR77YuyTX1EC7A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUEVrw/btsEKGgpVJM/rWU99LyvR77YuyTX1EC7A0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUEVrw/btsEKGgpVJM/rWU99LyvR77YuyTX1EC7A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUEVrw%2FbtsEKGgpVJM%2FrWU99LyvR77YuyTX1EC7A0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2418&quot; height=&quot;1188&quot; data-origin-width=&quot;2418&quot; data-origin-height=&quot;1188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 세그먼트가 모이면 하나의 루씬 인덱스가 됩니다. 루씬은 이 인덱스 내에서만 검색이 가능합니다. 엘라스틱서치 샤드는 루씬 인덱스 하나를 래핑한 단위입니다. 엘라스틱서치 샤드 여러 개가 모이면 엘라스틱서치 인덱스가 됩니다. 엘라스틱서치 레벨에서는 여러 샤드에 있는 문서를 모두 검색할 수 있고 새 문서가 들어오면 해당 내용을 라우팅하여 여러 샤드에 분산시켜 저장, 색인합니다. 이후 클라이언트가 엘라스틱서치에 검색 요청을 보내면 엘라스틱서치는 해당하는 각 샤드를 대상으로 검색을 한 뒤 그 결과를 모아 병합하여 최종 응답을 만듭니다. 이런 구조를 통해 루씬 레벨에서는 불가능한 분산 검색을 엘라스틱서치 레벨에서는 가능하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;translog&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dm1cIt/btsEFi88kn5/80TnVqehRGnMCs6od6gVCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dm1cIt/btsEFi88kn5/80TnVqehRGnMCs6od6gVCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dm1cIt/btsEFi88kn5/80TnVqehRGnMCs6od6gVCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdm1cIt%2FbtsEFi88kn5%2F80TnVqehRGnMCs6od6gVCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1434&quot; height=&quot;424&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치에 색인된 문서들은 루씬 commit까지 완료되어야 디스크에 안전하게 기록됩니다. 그렇다고 문서에 변경이 있을때마다 루씬 commit을 수행하기에는 비용이 부담되고 한번에 모아서 처리하자니 장애가 발생할 때 데이터 유실이 발생할 우려가 있습니다. 이런 문제를 해결하기 위해 엘라스틱서치 샤드는 모든 작업마다 translog라는 이름의 작업 로그를 남깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;translog는 색인, 삭제 작업이 루씬 인덱스에 수행된 직후에 기록됩니다. translog 기록까지 끝난 이후에야 작업 요청이 성공으로 승인됩니다. 엘라스틱서치에 장애가 발생한 경우 엘라스틱서치는 샤드 복구 단계에서 translog를 읽습니다. translog 기록은 성공했지만 루씬 commit에 포함되지 못했던 작업 내용이 있다면 샤드 복구 단계에서 복구됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;translog가 너무 크면 복구 시간이 오래걸리게 됩니다. 엘라스틱서치 flush는 루씬 commit을 수행하고 새로운 translog를 만드는 작업입니다. 엘라스틱서치 flush가 백그라운드에서 주기적으로 수행되며 translog의 크기를 적절한 수준으로 유지합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엘라스틱서치 구조&lt;/h2&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;622&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5SGNc/btsEFOGLyS4/M8r38RMwNhzIxi51SW4C61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5SGNc/btsEFOGLyS4/M8r38RMwNhzIxi51SW4C61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5SGNc/btsEFOGLyS4/M8r38RMwNhzIxi51SW4C61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5SGNc%2FbtsEFOGLyS4%2FM8r38RMwNhzIxi51SW4C61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;372&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 : 엘라스틱서치 프로세스 하나가 노드 하나를 구성합니다. 엘라스틱서치 노드 하나는 여러 개의 샤드를 가지며 고가용성을 위해 같은 종류의 샤드는 다른 노드에 배치합니다.&lt;/li&gt;
&lt;li&gt;클러스터 : 여러 노드가 모여 하나의 클러스터를 구성합니다.&lt;/li&gt;
&lt;li&gt;노드의 역할 : 엘라스틱서치 노드는 데이터 노드, 마스터 노드, 조정 노드 등 여러 역할 중 하나 이상의 역할을 맡아 수행합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 노드 : 샤드를 보유하고 샤드에 실제 읽기와 쓰기 작업을 수행&lt;/li&gt;
&lt;li&gt;마스터 노드 : 클러스터를 관리하는 역할&lt;/li&gt;
&lt;li&gt;조정 노드 : 클라이언트의 요청을 받아서 데이터 노드에 요청을 분배하고 클라이언트에게 응답을 돌려주는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZbhBj/btsEF68eXPp/jkX1PyTKI5jMzPPXBxFKbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZbhBj/btsEF68eXPp/jkX1PyTKI5jMzPPXBxFKbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZbhBj/btsEF68eXPp/jkX1PyTKI5jMzPPXBxFKbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZbhBj%2FbtsEF68eXPp%2FjkX1PyTKI5jMzPPXBxFKbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;493&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서(document) : 저장하고 색인을 생성하는 JSON 문서&lt;/li&gt;
&lt;li&gt;샤드 : 여러 문서의 모음&lt;/li&gt;
&lt;li&gt;인덱스 : 분산된 샤드에 저장된 문서들의 논리적입 집합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 인덱스에 여러 개의 샤드가 있고 이 샤드 안에는 여러 도큐먼트를 담아서 보관합니다. 그리고 샤드는 각 노드에 분산되어 저장됩니다.&lt;/p&gt;
&lt;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;RDBMS와 엘라스틱서치 비교&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RDBMS&lt;/th&gt;
&lt;th&gt;Elasticsearch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DBMS HA 구성(MMM, M/S)&lt;/td&gt;
&lt;td&gt;Cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DBMS Instance&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table&lt;/td&gt;
&lt;td&gt;Index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partition&lt;/td&gt;
&lt;td&gt;Shard/ Routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row&lt;/td&gt;
&lt;td&gt;Document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column&lt;/td&gt;
&lt;td&gt;Field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary Key&lt;/td&gt;
&lt;td&gt;_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema&lt;/td&gt;
&lt;td&gt;Mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노드별 하드웨어 권장 사양&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;노드&lt;/th&gt;
&lt;th&gt;cpu&lt;/th&gt;
&lt;th&gt;메모리&lt;/th&gt;
&lt;th&gt;저장장치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;마스터&lt;/td&gt;
&lt;td&gt;저사양&lt;/td&gt;
&lt;td&gt;저사양&lt;/td&gt;
&lt;td&gt;저사양&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;고사양&lt;/td&gt;
&lt;td&gt;고사양&lt;/td&gt;
&lt;td&gt;고사양&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조정&lt;/td&gt;
&lt;td&gt;저사양&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;저사양&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터 노드는 클러스터의 상태 관리가 주 역할이기 때문에 하드웨어 성능이 중요하진 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노드 warm cold 구성&lt;/b&gt;&lt;br /&gt;데이터 노드는 저장하는 데이터 성격에 따라 hot, warm, cold로 구분할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hot : 인덱싱과 검색이 활발&lt;/li&gt;
&lt;li&gt;warm : 자주 사용하지 않는 데이터, 쿼리 빈도가 낮고 인덱싱은 일어나지 않는 인덱스 저장&lt;/li&gt;
&lt;li&gt;cold : 검색을 수행하진 않지만 데이터 보존 기간 정책상 보관해야만 하는 freeze 모드의 인덱스들을 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;샤드 개수와 크기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ5itH/btsEE0Oqi0h/ihknJKaCElHla4VX3KB2L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ5itH/btsEE0Oqi0h/ihknJKaCElHla4VX3KB2L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ5itH/btsEE0Oqi0h/ihknJKaCElHla4VX3KB2L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ5itH%2FbtsEE0Oqi0h%2FihknJKaCElHla4VX3KB2L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;267&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치는 고가용성을 위해 샤드를 복제하여 여러 노드에 분산하여 저장합니다. 원본 샤드를 primary shard라고 하고 복제본을 replica shard라고 합니다. node1, node2를 보면 원본과 복제본이 서로 다른 노드에 위치한 것을 알 수 있습니다. 따라서 하나의 노드에 장애가 발생하더라도 복제본이 다른 노드에 존재하기 때문에 Primary shard가 유실된 경우 남아있던 복제본이 Primary shard로 승격되고 다른 노드에 새로 복제본을 생성하게 되면서 고가용성을 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤드는 다음과 같은 옵션으로 개수를 설정할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;number_of_shard : 프라이머리 샤드 개수&lt;/li&gt;
&lt;li&gt;number_of_replicas : 레플리카 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 인덱스에 위 옵션을 3, 2로 지정했다면 해당 인덱스는 프라이머리 샤드 3개에 대한 복제본이 2개씩 생기므로 프라이머리 3개, 레플리카 6개로 총 9개의 샤드가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;number_of_shard는 인덱스가 데이터를 몇 개의 샤드로 쪼갤 것인지를 지정하는 값이므로 신중하게 설계해야 합니다. 한 번 지정하면 reindex 같은 동작을 통해 인덱스를 통째로 재색인하는 등 특별한 작업을 수행하지 않는 한 바꿀 수 없기 때문입니다. 샤드 개수를 어떻게 지정하느냐는 엘라스틱서치 성능에도 영향을 미칩니다. 클러스터에 샤드 숫자가 너무 많아지면 클러스터 성능이 떨이지나 인덱스당 샤드 숫자를 적게 지정하면 샤드 하나의 크기가 커집니다. 샤드 크키가 커지면 장애 상황 등에서 샤드 복구에 많은 시간이 소요되므로 클러스터 안정성이 떨어지게 됩니다. 공식 문서에는 샤드 하나의 크기를 10GB ~ 40GB 정도를 권장하지만 20GB만 되어도 느리다는 감각이 느껴지므로 보통 수 GB 내외로 조정하는 것이 좋습니다. 또한 노드의 heap 1GB당 20개 이하의 shards를 들고 있는 것이 권장되지만 32g heap 기준으로 노드당 640개의 샤드를 가지고 있어야 하는데 이 또한 조금 빡빡하므로 더 적은 개수의 샤드를 갖는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 상황마다 다르기 때문에 직접 테스트해보면서 조정해보는 수밖에 없습니다. 성능에 문제가 있다면 샤드 수를 늘리거나 데이터 노드를 스케일 아웃/업하면서 최적의 수치를 찾아야 합니다. 데이터 노드의 cpu가 여유롭다면 색인과 검색작업을 수행하는 프라이머리 샤드의 개수를 늘려볼 수도 있고, cpu 사용량이 많다면 노드를 추가하는 작업을 진행할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필드 타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;엘라스틱서치 필드 타입 종류&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;심플 타입&lt;/td&gt;
&lt;td&gt;text, keyword, date, long, double, boolean, ip..&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;계층 구조를 지원하는 타입&lt;/td&gt;
&lt;td&gt;object, nested..&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그 외 특수한 타입&lt;/td&gt;
&lt;td&gt;geo_point, geo_shape, completion(자동완성 검색을 위한 특수타입)..&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;date&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;date 타입은 인입되는 데이터의 형식을 format이라는 옵션으로 지정합니다. format은 여러 형식으로 지정할 수 있으며 문서가 어떤 형식으로 들어오더라도 엘라스틱서치 내부적으로 UTC 시간대로 변환하는 과정을 거쳐 epoch milliseconds 형식의 long 숫자로 색인됩니다. format에는 java의 DateTimeFormatter로 인식 가능한 패턴을 사용할 수 있고 그 외에 빌트인 형식이 미리 갖춰져 있습니다.&lt;/p&gt;
&lt;table style=&quot;height: 169px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;종류&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;epoch_millis&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;밀리초 단위로 표현한 epoch 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;epoch_second&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;초 단위로 표현한 epoch 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;date_time&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;yyyyMMdd 형태로 표현한 날짜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;strict_date_time&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;yyyy-MM-dd'T'HH:mm:ss.SSSZZ로 표현한 날짜와 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;date_optional_time&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;최소 연 단위의 날짜를 포함해야 하며, 선택적으로 시간 정보도 포함하여 ISO datetime형태로 표현된 날짜와 시간 ex) yyyy-MM-dd 또는 yyyy-MM-dd'T'HH:mm:ss.SSSZ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;strict_date_optional_time&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;date_optional_time과 동일하지만 연, 월, 일이 각각 4자리, 2자리, 2자리임을 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;계층 구조를 지원하는 타입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;object 타입과 nested 타입은 계층 구조를 지원합니다. 기본적으로 타입을 명시하지 않는 경우 object 타입으로 매핑되는데 두 타입간의 배열을 처리하는 동작에서 차이가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 색인
PUT object_test/_doc/2
{
  &quot;spec&quot;: [
    {
      &quot;cores&quot;: 12,
      &quot;memory&quot;: 128,
      &quot;storage&quot;: 8000
    },
    {
      &quot;cores&quot;: 6,
      &quot;memory&quot;: 64,
      &quot;storage&quot;: 8000
    }
    ,{
      &quot;cores&quot;: 6,
      &quot;memory&quot;: 32,
      &quot;storage&quot;: 4000
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;// 검색
GET object_test/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;term&quot;: {
            &quot;spec.cores&quot;: &quot;6&quot;
          }
        },
        {
          &quot;term&quot;: {
            &quot;spec.memory&quot;: &quot;128&quot;
          }
        }
      ]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 색인 후 검색하면 spec.cores가 6개이며 spec.memory는 128인 문서를 검색하게 되므로 두 조건을 동시에 만족하는 객체는 존재하지 않으므로 결과는 비어는 것으로 예상할 수 있습니다. 하지만 실제 수행 결과는 위에 색인한 도큐먼트가 검색됩니다. 이는 object 타입의 평탄화 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;spec.cores&quot;: [12, 6, 6],
  &quot;spec.memory&quot;: [128, 64, 32],
  &quot;spec.storage&quot;: [8000, 8000, 4000],
}&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;object 타입의 경우는 위와 같이 데이터가 평탄화되기 때문에 cores 6과 memory 128인 경우가 존재하니 검색 결과에 포함되게 됩니다. 반면에 nested 타입의 경우 똑같이 실행하게 되면 일반적으로 예상하는 결과대로 결과는 비어있게 됩니다. 결과 자체는 비어있어서 예상한대로 동작한 것 같지만 검색 조건에서 spec.memory 를 64로 수정하면 검색이 되어야하지만 이 또한 결과는 비어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nested 타입은 객체 배열의 각 객체를 내부적으로 별도의 루씬 문서를 분리해 저장합니다. 배열의 원소가 100개라면 부모 문서까지 해서 101개의 문서가 내부적으로 생성됩니다. nested의 동작 방식은 엘라스틱서치 내에서도 특수하기 때문에 nested 쿼리라는 전용 쿼리를 이용해서 검색해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET object_test/_search
{
  &quot;query&quot;: {
    &quot;nested&quot;: {
      &quot;path&quot;: &quot;spec&quot;,
      &quot;query&quot;: {
        &quot;bool&quot;: {
          &quot;must&quot;: [
            {
              &quot;term&quot;: {
                &quot;spec.cores&quot;: &quot;6&quot;
              }
            },
            {
              &quot;term&quot;: {
                &quot;spec.memory&quot;: &quot;128&quot;
              }
            }
          ]
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nested 쿼리를 지정하고 path 부분에 검색 대상이 될 nested 타입의 필드를 지정하고 nested 아래 query 절에 기존에 사용하던 쿼리를 넣으면 됩니다. spec.memory 값을 64로 지정하면 검색에 걸리고, 128로 지정하면 걸리지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nested 타입은 내부적으로 각 객체를 별도의 문서로 분리해서 저장하기 때문에 성능에 문제가 있을 수 있습니다. 따라서 엘라스틱서치는 nested 타입의 무분별한 사용을 막기 위해 두 가지 제한을 걸어두었습니다. index.mapping.nested_fields.limit 설정은 한 인덱스에 nested 타입을 몇 개까지 지정할 수 있는지를 제한합니다. 기본값은 50입니다. index.mapping.nested_objects.limit 설정은 한 문서가 nested 객체를 몇 개까지 가질 수 있는지를 제한합니다. 기본값은 10000입니다. 이 값들을 무리하게 높이면 OOM 발생 위험이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 77px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;타입&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;object&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;nested&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;용도&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;일반적인 계층 구조에 사용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;배열 내 각 객체를 독립적으로 취급해야 하는 특수한 상황에 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;성능&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;상대적으로 가볍다&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;상대적으로 무겁다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;검색&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;일반적인 쿼리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;전용 nested 쿼리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;text타입과 keyword타입&lt;/h4&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;2250&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpb7QH/btsEELDWGBF/ypKCxBwmc2BBDmtUR6j33K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpb7QH/btsEELDWGBF/ypKCxBwmc2BBDmtUR6j33K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpb7QH/btsEELDWGBF/ypKCxBwmc2BBDmtUR6j33K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpb7QH%2FbtsEELDWGBF%2FypKCxBwmc2BBDmtUR6j33K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2250&quot; height=&quot;506&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;charater filter, tokenizer, token filter를 사용하여 analyzer를 만들 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 자료형을 담는 필드에는 text 타입과 keyword 타입이 있습니다. text 타입은 문자열 값이 그대로 역색인이 되지 않고 값을 분석하여 여러 토큰으로 쪼개지고 쪼개진 토큰으로 역색인을 구성합니다. 쪼개진 토큰에 지정한 필터를 적용하는 등의 후처리 작업 후 최종적으로 역색인에 들어가는 형태를 텀(term)이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 keyword로 지정된 필드에 들어온 문자열 값은 토큰으로 쪼개지지 않고 역색인을 구성합니다. 애널라이저로 분석하는 대신 노멀라이저를 적용합니다. 노멀라이저는 간단한 전처리만을 거친 뒤 커다란 단일 텀으로 역색인을 구성합니다. 만약 특정 노멀라이저를 지정하지 않으면 아무 작업도 수행하지 않고 전체 문자열로 역색인을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, hello world 라는 문자열에 대해서 text 타입이라면 hello로 검색했을 때 검색이 가능하지만 keyword 타입의 경우에는 검색이 불가능합니다. keyword 타입의 경우는 여러 토큰으로 쪼개지지 않고 hello world 그대로 역색인이 되어있기 때문에 정확히 hello world로 검색을 해야만 검색이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 두 타입은 정렬과 집계, 스크립트 작업을 수행할 때 동작의 차이가 있습니다. 보통 정렬과 집계, 스크립트 작업의 대상이 될 필드는 keyword 타입을 쓰는 편이 낫습니다. keyword 타입은 기본적으로 doc_values라는 캐시를 사용하고 text 타입은 fielddata라는 캐시를 사용하기 때문입니다.&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;doc_values&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치는 term을 보고 역색인에서 문서를 찾는 방식이나 정렬, 집계, 스크립트 작업 시에는 접근법이 다릅니다. 문서를 보고 필드 내의 term을 찾습니다. docs_values는 디스크를 기반으로 한 자료 구조로 파일 시스템 캐시를 통해 효율적으로 정렬, 집계, 스크립트 작업을 수행할 수 있게 설계되었습니다. 엘라스틱서치에서는 text와 annotated_text 타입을 제외한 거의 모든 필드 타입이 doc_values를 지원합니다.&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;fielddata&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text 타입은 docs_values를 사용할 수 없고 정렬, 집계, 스크립트 작업에서 fielddata 캐시를 사용합니다. fielddata를 사용해 해당 작업을 수행할 때는 전체를 읽어들여 힙 메모리에 올립니다. 이 때문에 OOM 등의 많은 문제를 발생시킬 수 있어 기본값은 비활성화 상태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애널라이저와 토크나이저&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJRLTs/btsEED653NO/UaYEK9KYbdC3dZKqKXz3Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJRLTs/btsEED653NO/UaYEK9KYbdC3dZKqKXz3Tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJRLTs/btsEED653NO/UaYEK9KYbdC3dZKqKXz3Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJRLTs%2FbtsEED653NO%2FUaYEK9KYbdC3dZKqKXz3Tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2250&quot; height=&quot;506&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애널라이저는 0개 이상의 캐릭터 필터, 1개의 토크나이저, 0개 이상의 토큰 필터로 구성됩니다. 동작 역시 순차적으로 적용됩니다. 애널라이저는 입력한 텍스트에 캐릭터 필터를 적용하여 문자열을 변형시킨 뒤 토크나이저를 적용하여 여러 토큰으로 쪼갭니다. 쪼개진 토큰의 스트림에 토큰 필터를 적용해서 토큰에 특정한 변형을 가한 결과가 최종적으로 분석 완료된 텀(term)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;캐릭터 필터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터 필터는 텍스트를 캐릭터의 스트림으로 받아서 특정한 문자를 추가, 변경, 삭제합니다. 0개 이상 지정할 수 있으므로 지정하지 않을 수도 있습니다. 엘라스틱서치에는 내장 빌트인 캐릭터 필터가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML strip : HTML 요소 안쪽 데이터를 꺼내고, HTML 엔티티도 디코딩합니다.&lt;/li&gt;
&lt;li&gt;mapping : 치환할 대상이 되는 문자와 치환 문자를 맵 형태로 선언합니다.&lt;/li&gt;
&lt;li&gt;patter replace : 정규 표현식을 이용해서 문자를 치환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토크나이저&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토크나이저는 캐릭터 스트림을 받아서 여러 토큰으로 쪼개어 토큰 스트림을 만듭니다. 애널라이저에는 한 개의 토크나이저만 지정할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;standard : default 토크나이저로 대부분의 문장 부호를 제거합니다.&lt;/li&gt;
&lt;li&gt;keyword : 텍스트를 쪼개지 않고 그대로 하나의 토큰으로 만듭니다.&lt;/li&gt;
&lt;li&gt;ngram : 텍스트를 min_gram 이상, max_gram 값 이하의 단위로 쪼갭니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2, 3을 값으로 하여 hello를 예시로 들면, he, hel, el, ell, ll, llo 토큰으로 쪼개집니다.&lt;/li&gt;
&lt;li&gt;token_chars 속성을 통해 토큰에 포함시킬 타입의 문자를 지정할 수 있습니다. 기본은 모든 문자를 포함합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;letter : 언어의 글자로 분류되는 문자&lt;/li&gt;
&lt;li&gt;digit : 숫자로 분류되는 문자&lt;/li&gt;
&lt;li&gt;whitespace : 띄어쓰기나 줄바꿈 문자 등 공백으로 인식되는 문자&lt;/li&gt;
&lt;li&gt;punctuation : !, ' 등의 문장 부호&lt;/li&gt;
&lt;li&gt;symbol : $나 루트 같은 기호&lt;/li&gt;
&lt;li&gt;custom : custom_token_chars를 설정해서 따로 지정한 커스텀 문자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;edge_ngram : ngram과 유사하나, 먼저 입력된 텍스트를 token_chars에 지정된 문자를 기준으로 단어를 쪼개고 min, max_gram 값을 기준으로 모든 토큰의 시작 글자를 단어의 시작 글자로 고정시켜 다시 쪼갭니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3, 4를 기준으로 letter 옵션으로 Hello, World! 를 쪼개면 Hel, Hell, Wor, Worl 의 토큰으로 쪼개집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;letter : 공백, 특수문자 등 언어의 글자로 분류되는 문자가 아닌 문자를 만났을 때 쪼갭니다.&lt;/li&gt;
&lt;li&gt;whitespace : 공백 문자를 만났을 때 쪼갭니다.&lt;/li&gt;
&lt;li&gt;pattern : 지정한 정규표현식을 단어의 구분자로 쪼갭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토큰 필터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 필터는 토큰 스트림을 받아서 토큰을 추가, 변경, 삭제 합니다. 하나의 애널라이저에 0개 이상 지정할 수 있습니다. 여러 개가 지정될 경우 순차적으로 적용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lowercase / uppercase : 토큰 내용을 소/대문자로 치환&lt;/li&gt;
&lt;li&gt;stop : 불용어를 제거합니다. (a, an, the, in ..)&lt;/li&gt;
&lt;li&gt;synonym : 유의어 사전 파일을 지정하여 지정된 유의어를 치환&lt;/li&gt;
&lt;li&gt;pattern_replace : 정규식을 사용하여 치환&lt;/li&gt;
&lt;li&gt;stemmer : 지원되는 몇몇 언어의 어간 추출을 수행(한국어 지원 X)&lt;/li&gt;
&lt;li&gt;trim : 토큰 전후의 공백 제거&lt;/li&gt;
&lt;li&gt;truncate : 지정한 길이로 토큰 자르기&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;내장 애널라이저&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애널라이저는 캐릭터 필터, 토크나이저, 토큰 필터의 조합으로 구성되고 엘라스틱서치에는 내장 애널라이저가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;standard(default) : standard 토크나이저 + lowercase 토큰 필터&lt;/li&gt;
&lt;li&gt;simple : letter가 아닌 문자 단위로 쪼갠 뒤 lowercase 토큰 필터&lt;/li&gt;
&lt;li&gt;whitespace : whitespace 토크나이저&lt;/li&gt;
&lt;li&gt;stop : standard 애널라이저와 동일 + stop 토큰 필터&lt;/li&gt;
&lt;li&gt;keyword : keyword 토크나이저&lt;/li&gt;
&lt;li&gt;pattern : patter 토크나이저 + lower 토큰 필터&lt;/li&gt;
&lt;li&gt;language : 여러 언어 분석 지원(한국어 X)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;PUT analyzer_test
{
  &quot;settings&quot;: {
    &quot;analysis&quot;: {
      &quot;analyzer&quot;: {
        &quot;default&quot;: {
          &quot;type&quot;: &quot;keyword&quot;
        }
      }
    }
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;defaultText&quot;: {
        &quot;type&quot;: &quot;text&quot;
      },
      &quot;standardText&quot;: {
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;standard&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;settings.analysis.analyer 설정에 커스텀 애널라이저를 추가할 수 있습니다. 여기에 default라는 이름으로 애널라이저를 지정하면 기본 애널라이저를 변경할 수 있습니다. 위에서는 기본값인 standard에서 keyword로 변경했습니다. 그리고 아래와 같이 커스텀 필터를 추가해서 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;PUT analyzer_test2
{
  &quot;settings&quot;: {
    &quot;analysis&quot;: {
      &quot;char_filter&quot;: {
        &quot;my_char_filter&quot;: {
          &quot;type&quot;: &quot;mapping&quot;,
          &quot;mappings&quot;: [
            &quot;i. =&amp;gt; 1.&quot;,
            &quot;ii. =&amp;gt; 2.&quot;
            ]
        }
      },
      &quot;analyzer&quot;: {
        &quot;my_analyzer&quot;: {
          &quot;char_filter&quot;: [
            &quot;my_char_filter&quot;
            ],
            &quot;tokenizer&quot;: &quot;whitespace&quot;,
            &quot;filter&quot;: [
              &quot;lowercase&quot;
            ]

        }
      }
    }
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;myText&quot;: {
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;my_analyzer&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;노멀라이저&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노멀라이저는 애널라이저와 비슷한 역할을 하지만 text타입이 아닌 keyword 타입에 적용한다는 차이가 있습니다. 또한 애널라이저와는 다르게 단일 토큰을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노멀라이저는 토크나이저 없이 캐릭터 필터와 토큰 필터로 구성됩니다. 최종적으로 단일 토큰을 생성해야 하기 때문에 애널라이저에서 사용했던 캐릭터 필터와 토큰 필터를 모두 조합해서 사용할 수는 없고 ASCII folding, lowercase, upppercase 등 글자 단위로 작업을 수행하는 필터만 사용할 수 있습니다. 엘라스틱서치에서 제공하는 빌트인 노멀라이저는 lowercase밖에 없고 커스텀 노멀라이저를 등록해서 사용해야 합니다. 애널라이저의 경우 text타입에 아무것도 등록하지 않으면 standard 애널라이저가 기본으로 적용되었지만 keyword 타입의 경우 아무것도 등록하지 않으면 어떤 노멀라이저도 동작하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;PUT analyzer_test4
{
  &quot;settings&quot;: {
    &quot;analysis&quot;: {
      &quot;normalizer&quot;: {
        &quot;my_normalizer&quot;: {
          &quot;type&quot;: &quot;custom&quot;,
          &quot;char_filter&quot;: [],
          &quot;filter&quot;: [
            &quot;asciifolding&quot;,
            &quot;uppercase&quot;
          ]
        }
      }
    }
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;myNormalizerKeyword&quot;: {
        &quot;type&quot;: &quot;keyword&quot;,
        &quot;normalizer&quot;: &quot;my_normalizer&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스 템플릿&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;인덱스를 생성할 때마다 설정, 매핑, 애널라이저 등록 등의 작업을 매번 지정하면 수고가 많이 들어갑니다. 템플릿을 사전에 정의해 두면 인덱스 생성 시 사전 정의한 설정대로 인덱스가 생성됩니다. index_patterns 부분에 인덱스 패턴을 지정하고 새로 생성되는 인덱스의 이름이 패턴에 부합하면 템플릿에 맞춰 인덱스가 생성됩니다. priority 값을 이용하면 여러 인덱스 템플릿 간 우선순위를 지정할 수 있습니다. priority 값이 높을수록 우선순위가 높습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;PUT _index_template/my_template
{
  &quot;index_patterns&quot;: [&quot;pattern_test_index-*&quot;],
  &quot;priority&quot;: 1,
  &quot;template&quot;: {
    &quot;settings&quot;: {
      &quot;number_of_shards&quot;: 2,
      &quot;number_of_replicas&quot;: 2
    },
    &quot;mappings&quot;: {
      &quot;properties&quot;: {
        &quot;myTextField&quot;: {
          &quot;type&quot;: &quot;text&quot;
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅은 엘라스틱서치가 인덱스를 구성하는 샤드 중 몇 번 샤드를 대상으로 작업을 수행할지 지정하기 위해 사용하는 값 입니다. 라우팅 값은 문서를 색인할 때 문서마다 하나씩 지정할 수 있습니다. 작업 대상 샤드 번호는 지정된 라우팅 값을 해시한 후 주 샤드의 개수로 나머지 연산을 수행한 값이 됩니다. 라우팅 값을 지정하지 않고 문서를 색인하는 경우 라우팅 기본값은 _id가 됩니다. 색인 시 라우팅 값을 지정했다면, 조회, 업데이트, 삭제, 검색 등의 작업에서도 똑같이 라우팅을 지정해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;PUT routing_test/_doc/1?routing=myId
{
  &quot;login_id&quot;:&quot;myId&quot;,
  &quot;comment&quot;: &quot;hello world&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤드를 가진 인덱스를 생성한 뒤 myid라는 값을 라우팅 값으로 지정하여 문서를 색인했습니다. 엘라스틱서치는 검색할 때 라우팅 값을 기입하지 않으면 전체 샤드를 대상으로 검색을 수행하지만 라우팅 값을 명시하면 단일 샤드를 대상으로 검색합니다. 따라서 제대로 명시했을 경우 검색 성능이 높아지지만, 색인한 라우팅 값과 다르게 라우팅값을 넣어서 검색할 경우 검색 결과에 원하는 문서가 포함되지 않을 수 있습니다. &lt;b&gt;운영 환경에서 많은 데이터가 저장되기 때문에 문서를 색인하거나 검색할 때는 가능한 한 라우팅 값을 지정해 주는 것이 권장됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 로그인한 사용자가 작성한 댓글을 색인하는 인덱스를 만드는 경우 특정 아이디가 작성한 댓글을 모아서 조회하는 요청이 많이 들어올 것이 예상된다면 로그인 아이디를 라우팅 값으로 지정하는 것이 좋습니다. 로그인 아이디가 동일한 문서끼리 같은 샤드에 위치시켜 검색 성능을 끌어올릴 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 엘라스틱서치에 인덱스를 설계하고 생성하는 주체, 인덱스에 데이터를 색인하는 주체, 색인된 데이터를 조회하는 주체가 각각 다를 수 있습니다. 따라서 담당자들 간에 라우팅 지정에 대한 정책을 세우고 조율해야 합니다. 그러나 다양한 조직의 여러 사람들과 협업하면서 일관된 정책을 유지하는 것은 쉽지 않고 라우팅이 누락되는 경우가 발생할 수 있습니다. 이런 일이 방생하는 것을 막기 위해 인덱스 매핑에서 routing 메타 필드를 지정하여 라우팅 값 명시를 필수로 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;puppet&quot;&gt;&lt;code&gt;PUT routing_test2
{
  &quot;mappings&quot;: {
    &quot;_routing&quot;: {
      &quot;required&quot;: true
    }
  }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Database</category>
      <category>ElasticSearch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/79</guid>
      <comments>https://backtony.tistory.com/79#entry79comment</comments>
      <pubDate>Sun, 11 Feb 2024 22:35:02 +0900</pubDate>
    </item>
    <item>
      <title>Spring Kafka</title>
      <link>https://backtony.tistory.com/78</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 포스팅은 spring boot 3.2.2 버전을 기준으로 작성되었습니다.&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;공부한 내용을 정리하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;와 관련 코드를 공유하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/kafka/spring/spring-kafka&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;스프링 카프카&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카는 카프카를 스프링 프레임워크에서 효과적으로 사용할 수 있도록 만들어진 라이브러리로 기존 카프카 클라이언트 라이브러리를 래핑해서 만들어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 카프카 프로듀서&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성&lt;/h3&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.kafka:spring-kafka'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;spring:
  kafka:
    bootstrap-servers: kafka.sample.url.com:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KafkaTemplate과 ProducerFactory 설정&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class CommonKafkaProducerConfig(
    private val kafkaProperties: KafkaProperties,
    private val sslBundles: SslBundles,
) {

    @Bean
    fun commonKafkaTemplate(): KafkaTemplate&amp;lt;String, Any&amp;gt; {
        return KafkaTemplate(commonProducerFactory())
    }

    @Bean
    fun commonProducerFactory(): ProducerFactory&amp;lt;String, Any&amp;gt; {
        val keySerializer = StringSerializer()
        val valueSerializer = JsonSerializer&amp;lt;Any&amp;gt;()

        return DefaultKafkaProducerFactory(kafkaProperties.buildProducerProperties(sslBundles), keySerializer, valueSerializer)
    }
}&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;스프링 카프카 프로듀서는 프로듀서 팩토리(ProducerFactory) 클래스를 사용하여 프로듀서의 설정값들을 세팅하고 카프카 템플릿(Kafka Template) 클래스를 사용하여 카프카 브로커로 메시지를 전송합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카의 properties 설정값들은 kafkaProperties에서 관리됩니다. application.yml에 설정한 bootstrap-servers값도 kafkaProperties에 주입되어 관리되며, 따로 설정하지 않은 값들은 kafkaProperties에 설정된 기본값으로 세팅됩니다. ProducerFactory에서 보내고자 하는 메시지의 키와 값타입에 따른 serializer를 등록하고 해당 producerFactory를 kafkaTemplate의 인자로 사용하면 세팅이 완료되고 해당 kafkaTemplate을 사용하여 카프카 브로커로 메시지를 전송할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JsonSerializer를 등록할 때, objectMapper를 별도로 주입해줄 수 있지만, 주입하지 않으면 kafka에서 제공하는 JsonSerializer는 내부적으로 plain한 ObjectMapper가 아닌 enhancedObjectMapper 메서드를 사용합니다. javaTimeModule, unknownProperties false등 세팅 등 일반적으로 objectMapper를 별도로 빈으로 등록해서 사용해야하는 경우에 대한 세팅이 대부분 들어가 있기 때문에 이외의 추가적인 세팅이 필요한 경우가 아니라면 그대로 사용해도 무방합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리스너 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Bean
fun commonKafkaTemplate(): KafkaTemplate&amp;lt;String, Any&amp;gt; {
  return KafkaTemplate(commonProducerFactory()).apply {
    setProducerListener(CommonKafkaListener())
  }
}

class CommonKafkaListener : ProducerListener&amp;lt;String, Any&amp;gt; {

    private val log = KotlinLogging.logger { }

    override fun onError(producerRecord: ProducerRecord&amp;lt;String, Any&amp;gt;, recordMetadata: RecordMetadata?, exception: java.lang.Exception?) {
        log.error(
            &quot;Fail to send kafka Message. Topic: ${producerRecord.topic()}, Partition: ${producerRecord.partition()},&quot; +
                &quot; Key: ${producerRecord.key()},  Value: ${producerRecord.value()}&quot;,
            exception,
        )
    }
}&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;ProducerListener를 구현하면 kafkaTemplate에 리스너를 붙여 사용할 수 있습니다.&lt;/p&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;actuator metric 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class CommonKafkaProducerConfig(
  private val kafkaProperties: KafkaProperties,
  private val meterRegistry: MeterRegistry,
  private val sslBundles: SslBundles,
) {

  @Bean
  fun commonProducerFactory(): ProducerFactory&amp;lt;String, Any&amp;gt; {
    val keySerializer = StringSerializer()
    val valueSerializer = JsonSerializer&amp;lt;Any&amp;gt;()

    return DefaultKafkaProducerFactory(kafkaProperties.buildProducerProperties(sslBundles), keySerializer, valueSerializer)
      .apply { addListener(MicrometerProducerListener(meterRegistry)) }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring actuator를 사용한다면 producerFactory에 MicrometerProducerListener를 붙여서 모니터링을 할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KafkaTemplate 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafkaTemplate의 send 메서드는 다양한 오버로딩을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;send(String topic, V data)&lt;/li&gt;
&lt;li&gt;send(String topic, K key, V data)&lt;/li&gt;
&lt;li&gt;send(String topic, Integer partition, K key, V data)&lt;/li&gt;
&lt;li&gt;send(String topic, Integer partition, Long timestamp, K key, V data)&lt;/li&gt;
&lt;li&gt;send(ProducerRecord&amp;lt;K,V&amp;gt; record)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic, key, value를 각각 받아서 처리하는 함수도 있고 producerRecord를 받아서 처리하는 함수도 있지만 결국 내부적으로는 producerRecord를 만들어서 전송하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;data class Article(
    val id: String = UUID.randomUUID().toString(),
    val title: String = UUID.randomUUID().toString(),
    val attachment: List&amp;lt;Attachment&amp;gt; = listOf(Attachment()),
    val registeredDate: LocalDateTime = LocalDateTime.now(),
) {
    data class Attachment(
        val id: String = UUID.randomUUID().toString(),
        val path: String = UUID.randomUUID().toString(),
    )
}

data class KafkaMessage(
  val topic: String,
  val key: String,
  val data: Any,
  val headers: MutableMap&amp;lt;String, String&amp;gt; = mutableMapOf(),
) {
  fun buildProducerRecord(): ProducerRecord&amp;lt;String, Any&amp;gt; {
    return ProducerRecord(topic, key, data).apply {
      headers.entries.forEach {
        this.headers().add(it.key, it.value.toByteArray())
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 객체를 예시로 한다면 아래와 같이 메시지를 발송할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class SamplePublisher(
  private val commonKafkaTemplate: KafkaTemplate&amp;lt;String, Any&amp;gt;,
) {

  @PostMapping(&quot;/articles&quot;)
  fun publishMessage() {
    val messages = mutableListOf&amp;lt;KafkaMessage&amp;gt;()

    repeat(2) {
      val article = Article()
      messages.add(KafkaMessage(
        topic = &quot;backtony-test&quot;,
        key = article.id,
        data = article,
      ))
    }

    for (message in messages) {
      commonKafkaTemplate.send(message.buildProducerRecord())
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 카프카 컨슈머&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성&lt;/h3&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.kafka:spring-kafka'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;spring:
  kafka:
    bootstrap-servers: kafka.sample.url.com:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;commonKafkaListenerContainerFactory와 ConsumerFactory 설정&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@EnableKafka // @KafkaListener 애노테이션 활성화
@Configuration
class ConsumerConfig(
  private val kafkaProperties: KafkaProperties,
  private val meterRegistry: MeterRegistry,
  private val sslBundles: SslBundles,
) {

  @Bean(COMMON)
  fun commonKafkaListenerContainerFactory(
    commonConsumerFactory: ConsumerFactory&amp;lt;String, Any&amp;gt;,
  ): KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, Any&amp;gt;&amp;gt; {
    return ConcurrentKafkaListenerContainerFactory&amp;lt;String, Any&amp;gt;().apply {
      consumerFactory = commonConsumerFactory
    }
  }

  @Bean
  fun commonConsumerFactory(): ConsumerFactory&amp;lt;String, Any&amp;gt; {
    val keyDeserializer = StringDeserializer()
    val valueDeserializer = JsonDeserializer(Any::class.java).apply {
      addTrustedPackages(&quot;com.example.*&quot;) // JsonDeserializer 주의사항 파트에서 따로 설명
    }

    return DefaultKafkaConsumerFactory(getCommonConsumerConfigs(), keyDeserializer, valueDeserializer)
      .apply { addListener(MicrometerConsumerListener(meterRegistry)) }
  }

  private fun getCommonConsumerConfigs(): Map&amp;lt;String, Any&amp;gt; {
    return kafkaProperties.buildConsumerProperties(sslBundles)
  }

  companion object {
    const val COMMON = &quot;commonKafkaListenerContainerFactory&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카 컨슈머는 컨슈머 팩토리(ConsumerFactory) 클래스를 사용하여 컨슈머의 설정값들을 세팅하고 카프카 리스터 컨테이너 팩토리(KafkaListenerContainerFactory) 클래스를 브로커로부터 메시지를 수신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카의 properties 설정값들은 kafkaProperties에서 관리됩니다. application.yml에 설정한 bootstrap-servers값도 kafkaProperties에 주입되어 관리되며, 따로 설정하지 않은 값들은 kafkaProperties에 설정된 기본값으로 세팅됩니다. ConsumerFactory에서 수신하고자 하는 메시지 키와 값타입에 따른 deserializer를 등록하고 해당 ConsumerFactory를 KafkaListenerContainerFactory의 인자로 사용하면 세팅이 완료되고 카프카 브로커로부터 메시지를 수신할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머와 마찬가지로 spring actuator를 사용하는 경우 MicrometerConsumerListener를 추가하여 모니터링할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리스너 타입과 offset 커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카의 컨슈머는 기존 컨슈머를 크게 2개의 타입으로 나누고 커밋을 7가지로 나누어 세분화했습니다.&amp;nbsp;&amp;nbsp;리스너&amp;nbsp;타입에&amp;nbsp;따라&amp;nbsp;한번&amp;nbsp;호출하는&amp;nbsp;메서드에서&amp;nbsp;처리하는&amp;nbsp;레코드의&amp;nbsp;개수가&amp;nbsp;달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레코드 리스너(MessageListener) : 단 1개의 레코드 처리, Default 값&lt;/li&gt;
&lt;li&gt;배치 리스너(BatchMessageListener) : 한 번에 여러 개의 레코드를 처리&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1710056894951&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(COMMON)
fun commonKafkaListenerContainerFactory(
    commonConsumerFactory: ConsumerFactory&amp;lt;String, Any&amp;gt;,
    commonErrorHandler: CommonErrorHandler,
): KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, Any&amp;gt;&amp;gt; {
    return ConcurrentKafkaListenerContainerFactory&amp;lt;String, Any&amp;gt;().apply {
        consumerFactory = commonConsumerFactory
        isBatchListener = false // default false
        setCommonErrorHandler(commonErrorHandler)
    }
}&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;옵션은 factory를 빈으로 생성할 때, 설정할 수 있습니다. 해당 값은 @KafkaListener의 옵션으로 override할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;스프링&amp;nbsp;카프카&amp;nbsp;컨슈머의&amp;nbsp;기본&amp;nbsp;리스너&amp;nbsp;타입은&amp;nbsp;레코드&amp;nbsp;리스너이고&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;파생된&amp;nbsp;여러&amp;nbsp;형태가&amp;nbsp;있습니다.&amp;nbsp;&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;Record 타입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MessageListener : Record 인스턴스 단위로 프로세싱, 오토커밋 또는 컨슈머 컨테이너의 ackMode를 사용하는 경우&lt;/li&gt;
&lt;li&gt;AcknowledgingMessageListener : Record 인스턴스 단위로 프로세싱, 메뉴얼 커밋을 사용하는 경우&lt;/li&gt;
&lt;li&gt;ConsumerAwareMessageListener : Record 인스턴스 단위로 프로세싱, 컨슈머 객체를 활용하고 싶은 경우&lt;/li&gt;
&lt;li&gt;AcknowledgingConsumerAwareMessageListener : Record 인스턴스 단위로 프로세싱, 매뉴얼 커밋을 사용하고 컨슈머 객체를 활용하고 싶은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;batch 타입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BatchMessageListener : Records 인스턴스 단위로 프로세싱, 오토 커밋 또는 컨슈머 컨테이너의 AckMode를 사용하는 경우&lt;/li&gt;
&lt;li&gt;BatchAcknowledgingMessageListener : Records 인스턴스 단위로 프로세싱, 매뉴얼 커밋을 사용하는 경우&lt;/li&gt;
&lt;li&gt;BatchConsumerAwareMessageListener : Records 인스턴스 단위로 프로세싱, 컨슈머 객체를 활용하고 싶은 경우&lt;/li&gt;
&lt;li&gt;&lt;span&gt;BatchAcknowledgingConsumerAwareMessageListener : Records 인스턴스 단위로 프로세싱, 매뉴얼 커밋을 사용하고 컨슈머 객체를 활용하고 싶은 경우&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메뉴얼 커밋이란 자동 이 아닌 개발자가 명시적으로 커밋하는 방식을 의미합니다. 메뉴얼&amp;nbsp;커밋을&amp;nbsp;사용할&amp;nbsp;경우에는&amp;nbsp;Acknowledging이&amp;nbsp;붙은&amp;nbsp;리스너를&amp;nbsp;사용하고,&amp;nbsp;Kafka&amp;nbsp;Cosumer&amp;nbsp;인스턴스에&amp;nbsp;직접&amp;nbsp;접근하여&amp;nbsp;컨트롤하고&amp;nbsp;싶다면&amp;nbsp;ConsumerAware가&amp;nbsp;붙은&amp;nbsp;리스너를&amp;nbsp;사용하면&amp;nbsp;됩니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring kafka consumer는 메시지를 소비하고 브로커에게 메시지를 소비했다고 커밋하는 과정이 필요합니다. s&lt;b&gt;pring kafka consumer에서 자동으로 일정 시간 이후에 commit하는 옵션인 enable.auto.commit 옵션은 2.3 버전 이후부터 false가 default값으로 변경되었고 AckMode를 통해 컨트롤 됩니다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;auto.commit이 true 인 경우, AckMode는 무시되며, auto.commit.interval 옵션에 의해 interval 시간마다 커밋됩니다. 반면에 enable.auto.commit옵션이 false인 경우, AckMode에 의해 커밋 주기가 결정됩니다.&lt;/b&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%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;AckMode&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;RECORD&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;레코드&amp;nbsp;단위로&amp;nbsp;프로세싱&amp;nbsp;이후&amp;nbsp;커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;BATCH&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;스프링&amp;nbsp;카프카의&amp;nbsp;기본값&lt;br /&gt;poll 메서드로 호출된 레코드가 모두 처리된 이후 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;TIME&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;poll 메서드로 반환된 모든 레코드를 처리한 후, ackTime 간격마다 처리 완료된 메시지들의 오프셋만 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;COUNT&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;특정&amp;nbsp;개수만큼&amp;nbsp;레코드가&amp;nbsp;처리된&amp;nbsp;이후에&amp;nbsp;커밋&lt;br /&gt;이&amp;nbsp;옵션을&amp;nbsp;사용할&amp;nbsp;경우에는&amp;nbsp;레코드&amp;nbsp;개수를&amp;nbsp;선언하는&amp;nbsp;AckCount&amp;nbsp;옵션을&amp;nbsp;설정해야&amp;nbsp;합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;MANUAL&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;BATCH 옵션과 유사하나 커밋 처리를 직접적으로 Acknowledgement.acknowledge() 메서드 호출로 명시해야 합니다.&lt;br /&gt;이 옵션을 사용할 경우에는 AcknowledgingMessageListener 또는 BatchAcknowledgingMessageListener를 리스너로 사용해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;MANUAL_IMMEDIATE&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Acknowledgement.acknowledge&amp;nbsp;메서드를&amp;nbsp;호출한&amp;nbsp;즉시&amp;nbsp;커밋합니다.&lt;br /&gt;이&amp;nbsp;옵션을&amp;nbsp;사용할&amp;nbsp;경우에는&amp;nbsp;AcknowledgingMessageListener&amp;nbsp;또는&amp;nbsp;BatchAcknowledgingMessageListener를&amp;nbsp;리스너로&amp;nbsp;사용해야&amp;nbsp;합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1710056620847&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(COMMON)
fun commonKafkaListenerContainerFactory(
  commonConsumerFactory: ConsumerFactory&amp;lt;String, Any&amp;gt;,
): KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, Any&amp;gt;&amp;gt; {
  return ConcurrentKafkaListenerContainerFactory&amp;lt;String, Any&amp;gt;().apply {
    consumerFactory = commonConsumerFactory
    containerProperties.ackMode = AckMode.BATCH
  }
}&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;Default값은 Batch이고 위와 같이 factory를 만드는 시점에 옵션으로 지정해줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@KafkaListener&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class SampleListener {

    private val log = KotlinLogging.logger { }

    @KafkaListener(
        groupId = &quot;backtony-test-single&quot;,
        topics = [&quot;backtony-test&quot;],
        containerFactory = COMMON,
    )
    fun sample(record: Article) {
        log.info { record }
    }

    @KafkaListener(
        groupId = &quot;backtony-test-batch&quot;,
        topics = [&quot;backtony-test&quot;],
        containerFactory = COMMON,
        batch = &quot;true&quot;,
    )
    fun sampleBatch(event: List&amp;lt;Article&amp;gt;) {
        log.info { &quot;batch count : ${event.size}&quot; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 정의한 KafkaListenerContainerFactory 세팅을 통해서 카프카 브로커로부터 메시지를 받을 수 있는 구조가 만들어졌습니다. ConsumerConfig 클래스에 @EnableKafka 애노테이션을 붙였기 때문에 @KafkaListener를 사용할 수 있습니다. @KafkaListener 옵션으로 컨슈머 그룹, 토픽, containerFactory를 명시하면 카프카브로커로부터 메시지 소비가 시작됩니다. containerFactory에는 앞서 정의한 commonKafkaListenerContainerFactory의 빈 이름을 명시해주면 됩니다. batch 옵션은 메시지를 단건으로 받아서 처리할지, 다건(리스트)로 받아서 처리할지 여부를 의미합니다. 해당 옵션의 앞서 언급했던 factory 빈 생성 시 isBatchListener 옵션에 해당하는 값을 override합니다. factory 빈 생성 시 사용한 옵션을 사용하고 없다면 default값은 false입니다. 만약 true로 값을 세팅했다면 listener에서는 반드시 List 형태로 메시지를 소비해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DefaultErrorHandler&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머에서 로직을 처리하다가 문제가 발생했을 경우, 처리할 수 있도록 컨슈머에 CommonErrorHandler를 정의할 수 있고 여러 구현체가 제공됩니다. 보통 DefaultErrorHandler를 사용하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public DefaultErrorHandler(@Nullable ConsumerRecordRecoverer recoverer, BackOff backOff) {
    this(recoverer, backOff, null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 생성자가 있지만 위 생성자를 사용해보겠습니다. 예외가 발생했을 때, 수행할 동작을 정의하는 recoverer와 재시도 BackOff를 인자로 넘겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class CommonConsumerRecordRecoverer : ConsumerRecordRecoverer {

    private val log = KotlinLogging.logger { }

    override fun accept(record: ConsumerRecord&amp;lt;*, *&amp;gt;, ex: Exception) {

        var groupId: String? = &quot;&quot;
        if (ex is ListenerExecutionFailedException) {
            groupId = ex.groupId
        }

        log.error(
            &quot;[Consumer error] occurred error while consuming message. &quot; +
                &quot;topic : ${record.topic()}, groupId : $groupId, offset : ${record.offset()}, &quot; +
                &quot;key : ${record.key()}, value : ${record.value()}, error message : ${ex.message}&quot;,
            ex,
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 ConsumerRecordRecoverer를 구현하여 예외가 발생했을 때, 로깅을 남기는 recoverer를 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Bean(COMMON)
fun commonKafkaListenerContainerFactory(
  commonConsumerFactory: ConsumerFactory&amp;lt;String, Any&amp;gt;,
  commonErrorHandler: CommonErrorHandler,
): KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, Any&amp;gt;&amp;gt; {
  return ConcurrentKafkaListenerContainerFactory&amp;lt;String, Any&amp;gt;().apply {
    consumerFactory = commonConsumerFactory
    setCommonErrorHandler(commonErrorHandler) // 추가
  }
}

@Bean
fun commonErrorHandler(): CommonErrorHandler {
    return DefaultErrorHandler(commonConsumerRecordRecoverer, FixedBackOff(1000L, 3L)) // 1초 간격으로 최대 3회 재시도
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 KafkaListenerContainerFactory 정의 시점에 commonErrorHandler로 등록해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예외 발생 후 재처리가 안되는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AckMode가 기본값인 Batch를 사용하고, 위와 같은 commonErrorHandler를 등록해서 사용하는 경우, 메시지 처리 과정에서 예외가 발생하면 offset이 커밋되고 이후 소비에서는 예외가 발생한 메시지를 소비하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AckMode 기본값인 Batch 모드는 poll 메서드로 호출된 레코드가 모두 처리된 이후 커밋합니다. 즉, 메시지 처리 과정에서 예외가 발생하면 커밋이 되지 않아야 정상입니다. 하지만&amp;nbsp; CommonErrorHandler에는 ackAfterHandler 옵션이 기본값인 true로 세팅되어있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kwz1E/btsFF2jhMrZ/W4q7Nx8g9ra2SwEA2favJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kwz1E/btsFF2jhMrZ/W4q7Nx8g9ra2SwEA2favJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kwz1E/btsFF2jhMrZ/W4q7Nx8g9ra2SwEA2favJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkwz1E%2FbtsFF2jhMrZ%2FW4q7Nx8g9ra2SwEA2favJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1328&quot; height=&quot;224&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;224&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;2154&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mllXd/btsFEjNjTur/qQ2rjlxN8z5tnDNEKkIF3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mllXd/btsFEjNjTur/qQ2rjlxN8z5tnDNEKkIF3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mllXd/btsFEjNjTur/qQ2rjlxN8z5tnDNEKkIF3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmllXd%2FbtsFEjNjTur%2FqQ2rjlxN8z5tnDNEKkIF3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2154&quot; height=&quot;1054&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;1054&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;CommonErrorHandler는 예외를 받아서 처리하는데 ackAfterHandle 값이 true인 경우, 예외 처리기에서 처리를 완료해서 예외를 던지지 않으면 offset을 커밋하는 옵션입니다. 따라서 위의 경우에는 DefaultErrorHandler에 등록된 recoverer에서 로깅만 하고 넘기므로 offset이 커밋되기 때문에 이후 소비에서 예외가 발생한 메시지를 다시 소비하지 않게 됩니다. 만약 recoverer에서 다시 예외를 발생시킨다면 offset은 커밋되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JsonDeserializer 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서에서 jsonSerializer를 사용할 경우, 카프카 브로커로 메시지를 직렬화화여 전송할 때 kafkaHeader에 해당 객체의 타입 정보가 들어가게 됩니다. 그리고 컨슈머에서 이를 역직렬화하는 과정에서는 헤더에 들어있는 타입 정보를 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 멀티모듈 구조인 경우, 프로듀서에서 발송한 메시지 객체를 컨슈머에서 공유해서 사용합니다. 이 경우에는 JsonDeserializer가 헤더에 들어있는 타입의 패키지 경로를 신뢰할 수 있도록 등록해줘야 합니다. 따라서 JsonDeserilizer를 등록할 때, 아래와 같이 카프카 프로듀서에서 전송한 객체의 패키지 경로를 신뢰할 수 있도록 등록해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val valueDeserializer = JsonDeserializer(Any::class.java).apply {
    addTrustedPackages(&quot;com.example.*&quot;)
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에&amp;nbsp;프로젝트가&amp;nbsp;MSA&amp;nbsp;환경이라&amp;nbsp;다른&amp;nbsp;팀에서&amp;nbsp;보낸&amp;nbsp;메시지를&amp;nbsp;우리&amp;nbsp;팀에서&amp;nbsp;수신해야&amp;nbsp;하는&amp;nbsp;경우가&amp;nbsp;있습니다.&amp;nbsp;이&amp;nbsp;경우에는&amp;nbsp;멀티모듈&amp;nbsp;구조와&amp;nbsp;달리&amp;nbsp;해당&amp;nbsp;객체를&amp;nbsp;공유해서&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;없습니다.&amp;nbsp;이&amp;nbsp;경우에는&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;방법을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&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;메시지 값타입을 String 값으로 받아서 consumer 처리 로직에서 objectMapper로 타입을 직접 변환하는 방식&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/kafka/serdes.html#using-headers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;header를 이용한 type Mapping 방식&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방법은 producer와 consumer에서 모두 설정해줘야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/kafka/serdes.html#by-type&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;topic별 매핑 방식&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;consumer 쪽에서만 설정하면 되나, topic별 단일 deserilizer를 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/kafka/serdes.html#messaging-message-conversion&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;messaging message conversion 방식&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의&amp;nbsp;경우&amp;nbsp;는&amp;nbsp;ConsumerConfig에서&amp;nbsp;설정한&amp;nbsp;factory들의&amp;nbsp;value&amp;nbsp;타입을&amp;nbsp;String으로&amp;nbsp;바꾸고&amp;nbsp;@kafkaListener가&amp;nbsp;붙은&amp;nbsp;함수에서&amp;nbsp;메시지를&amp;nbsp;String으로&amp;nbsp;받아서&amp;nbsp;ObjectMapper로&amp;nbsp;직접&amp;nbsp;타입을&amp;nbsp;변환하고&amp;nbsp;처리하면&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;이외의&amp;nbsp;방법&amp;nbsp;중에서는&amp;nbsp;마지막&amp;nbsp;방법이&amp;nbsp;가장&amp;nbsp;수정&amp;nbsp;범위가&amp;nbsp;적습니다.&amp;nbsp;header&amp;nbsp;type&amp;nbsp;매핑&amp;nbsp;방식을&amp;nbsp;사용하면&amp;nbsp;@KafkaHandler를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있지만&amp;nbsp;producer에&amp;nbsp;해당하는&amp;nbsp;팀과&amp;nbsp;별도의&amp;nbsp;협의가&amp;nbsp;필요합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723373623028&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableKafka
@Configuration
class ConsumerConfig(
    private val kafkaProperties: KafkaProperties,
    private val meterRegistry: MeterRegistry,
    private val sslBundles: SslBundles,
    private val commonConsumerRecordRecoverer: ConsumerRecordRecoverer,
) {

    @Bean(COMMON)
    fun commonKafkaListenerContainerFactory(
        commonConsumerFactory: ConsumerFactory&amp;lt;String, Bytes&amp;gt;,
        commonErrorHandler: CommonErrorHandler,
    ): KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, Bytes&amp;gt;&amp;gt; {
        return ConcurrentKafkaListenerContainerFactory&amp;lt;String, Bytes&amp;gt;().apply {
            consumerFactory = commonConsumerFactory
            setRecordMessageConverter(JsonMessageConverter()) // jsonMessageConverter 등록 필수 
            setBatchMessageConverter(BatchMessagingMessageConverter(JsonMessageConverter())) // jsonMessageConverter 등록 필수
            setCommonErrorHandler(commonErrorHandler)
        }
    }
 
    @Bean
    fun commonConsumerFactory(): ConsumerFactory&amp;lt;String, Bytes&amp;gt; {
        return DefaultKafkaConsumerFactory(getCommonConsumerConfigs(), StringDeserializer(), getBytesValueDeserializer())
            .apply { addListener(MicrometerConsumerListener(meterRegistry)) }
    }

    private fun getBytesValueDeserializer(): Deserializer&amp;lt;Bytes&amp;gt; {
        return ErrorHandlingDeserializer(
            BytesDeserializer()
        )
    }

    private fun getCommonConsumerConfigs(): Map&amp;lt;String, Any&amp;gt; {
        return kafkaProperties.buildConsumerProperties(sslBundles)
            .apply { put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, LoggingConsumerInterceptor::class.java.name) }
    }

    @Bean
    fun commonErrorHandler(): CommonErrorHandler {
        return DefaultErrorHandler(commonConsumerRecordRecoverer, FixedBackOff(1000L, 3L))
    }

    companion object {
        const val COMMON = &quot;commonKafkaListenerContainerFactory&quot;
        const val MANUAL_ACK = &quot;manualAckKafkaListenerContainerFactory&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;jsonMessageConverter&amp;nbsp;등록으로&amp;nbsp;인해&amp;nbsp;타입&amp;nbsp;추론을&amp;nbsp;통해&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;방식으로&amp;nbsp;구현이&amp;nbsp;가능합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723373647317&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class SampleListener {

    private val log = KotlinLogging.logger { }

    @KafkaListener(
        groupId = &quot;backtony-test-single&quot;,
        topics = [&quot;backtony-test&quot;],
        containerFactory = COMMON,
    )
    fun sample(article: Article) {
        log.info { article.id }
    }

    @KafkaListener(
        groupId = &quot;backtony-test-batch&quot;,
        topics = [&quot;backtony-test&quot;],
        containerFactory = COMMON,
        batch = &quot;true&quot;,
    )
    fun sampleBatch(articles: List&amp;lt;Article&amp;gt;) {

        for (article in articles) {
            log.info { &quot;articleId : ${article.id}&quot; }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ErrorHandlingDeserializer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커로부터 직렬화된 데이터 수집 -&amp;gt; 데이터 역직렬화 -&amp;gt; 데이터 처리 -&amp;gt; 브로커에 commit 요청&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머는 대략 위와 같은 흐름으로 진행되고, 3번 과정인 데이터 처리에서 예외가 발생할 경우, 지정한 ErrorHandler에 의해 retry 횟수만큼 재시도하고 커밋하게 됩니다. 하지만 데이터 역직렬화 단계에서 실패한 경우 DeserializeException가 발생하면서 데이터 처리에서 발생한 예외가 아니기 때문에 ErrorHandler까지 도달하지 못하고 결국 commit되지 못해 같은 offset을 컨슈머가 계속 소비하게 되는 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class ErrorHandlingDeserializer&amp;lt;T&amp;gt; implements Deserializer&amp;lt;T&amp;gt; {

    // .. 생략
    private Deserializer &amp;lt; T &amp;gt; delegate;

    public ErrorHandlingDeserializer (Deserializer&amp;lt;T&amp;gt; delegate) {
        this.delegate = setupDelegate(delegate);
    }
}&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;ErrorHandlingDeserializer가 이러한 문제를 해결합니다. ErrorHandlingDeserializer는 역직렬화의 처리를 delegate deserializer로 위임하고 역직렬화 실패 시, null을 반환하도록 설계되었습니다. 이를 통해, 결과는 null이지만 역직렬화 과정은 통과하여 데이터 처리 단계까지 도달할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Bean
fun commonConsumerFactory(): ConsumerFactory&amp;lt;String, Any&amp;gt; {
    return DefaultKafkaConsumerFactory(getCommonConsumerConfigs(), StringDeserializer(), ErrorHandlingDeserializer(JsonDeserializer(Any::class.java))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 방법은 consumerFactory에 Deserilizer를 넘겨줄 때, ErrorHandlingDeserializer로 한번 감싸서 넘겨주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;concurrency&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaListenerContainerFactory는 KafkaListenerContainerFactory, ConcurrentKafkaListenerContainerFactory 두가지 타입을 제공합니다.&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;1412&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZgWdY/btsEEH2vNqP/h3bM9kd8iv9l3xA8TDtVzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZgWdY/btsEEH2vNqP/h3bM9kd8iv9l3xA8TDtVzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZgWdY/btsEEH2vNqP/h3bM9kd8iv9l3xA8TDtVzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZgWdY%2FbtsEEH2vNqP%2Fh3bM9kd8iv9l3xA8TDtVzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1412&quot; height=&quot;580&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaListenerContainerFactory는 단일 스레드로 동작합니다.(concurrency 옵션이 없습니다.) 따라서 소비해야할 메시지가 많은 경우, 컨슈머 랙이 발생할 수 있습니다.&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;1428&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2iRrX/btsEGPZa5kF/pTaKV0GxdpWytaeSBnrYy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2iRrX/btsEGPZa5kF/pTaKV0GxdpWytaeSBnrYy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2iRrX/btsEGPZa5kF/pTaKV0GxdpWytaeSBnrYy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2iRrX%2FbtsEGPZa5kF%2FpTaKV0GxdpWytaeSBnrYy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1428&quot; height=&quot;580&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentKafkaListenerContainerFactory는 멀티 스레드로 동작합니다.(concurrency 옵션이 있습니다.) 따라서 소비해야할 메시지가 많은 경우 적합합니다. 카프카 컨슈머 모델에서는 한 파티션을 동시에 여러 컨슈머 스레드가 처리할 수 없습니다. 따라서 파티션의 개수보다 스레드 수가 많아지면 나머지 스레드는 놀게 되면서 자원이 낭비되게 됩니다. 예를 들어, 파티션이 3개이고 concurrency가 5라면 나머지 2개의 스레드는 놀게 되면서 자원이 낭비되게 됩니다. 따라서 concurrency는 컨슈머에 매핑된 파티션의 개수보다 작거나 같아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@KafkaListener(
    groupId = &quot;backtony-test-single&quot;,
    topics = [&quot;backtony-test&quot;],
    containerFactory = COMMON,
    concurrency = &quot;3&quot; // concurrency 설정
)
fun sample(record: Article) {
    log.info { record }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터셉터&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Bean
fun commonConsumerFactory(): ConsumerFactory&amp;lt;String, Any&amp;gt; {
  return DefaultKafkaConsumerFactory(getCommonConsumerConfigs(), StringDeserializer(), getJsonValueDeserializer())
}

private fun getCommonConsumerConfigs(): Map&amp;lt;String, Any&amp;gt; {
  return kafkaProperties.buildConsumerProperties(sslBundles)
    .apply { put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, LoggingConsumerInterceptor::class.java.name) }
}

class LoggingConsumerInterceptor : ConsumerInterceptor&amp;lt;String, Any&amp;gt; {

    private val log = KotlinLogging.logger { }

    override fun configure(configs: MutableMap&amp;lt;String, *&amp;gt;) {}

    override fun close() {}

    override fun onCommit(offsets: MutableMap&amp;lt;TopicPartition, OffsetAndMetadata&amp;gt;?) {}

    override fun onConsume(records: ConsumerRecords&amp;lt;String, Any&amp;gt;): ConsumerRecords&amp;lt;String, Any&amp;gt; {
        records.forEach {
            log.info(&quot;Start consuming the message: ${it.value()}&quot;)
        }
        return records
    }
}&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;ConsumerInterceptor를 구현하여 인터셉터를 만들 수 있고 ConsumerFactory를 만들 때, config에 추가하면 적용할 수 있습니다.&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Kafka</category>
      <category>spring</category>
      <category>spring kafka</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/78</guid>
      <comments>https://backtony.tistory.com/78#entry78comment</comments>
      <pubDate>Sat, 10 Feb 2024 16:45:26 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - consumer</title>
      <link>https://backtony.tistory.com/77</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨슈머 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS3pQ1/btsEGi8daxB/zMKOVKo5I3ExFqkSgeo9MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS3pQ1/btsEGi8daxB/zMKOVKo5I3ExFqkSgeo9MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS3pQ1/btsEGi8daxB/zMKOVKo5I3ExFqkSgeo9MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS3pQ1%2FbtsEGi8daxB%2FzMKOVKo5I3ExFqkSgeo9MK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2096&quot; height=&quot;1166&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카에는 1개 이상의 컨슈머로 이뤄진 컨슈머 그룹이 존재합니다. 컨슈머 그룹을 운영함으로 인해 각 컨슈머 그룹으로부터 격리된 환경에서 안전하게 운영할 수 있습니다. 컨슈머 그룹으로 묶인 컨슈머들은 토픽에 있는 1개 이상 파티션들에 할당되어 데이터를 가져갈 수 있습니다. 반면에 파티션은 최대 1개의 컨슈머에만 할당 가능합니다. 이러한 특징으로 컨슈머 그룹의 컨슈머 개수는 가져가고자 하는 토픽의 파티션 개수와 같거나 작아야 합니다. 만약 그보다 많아지게 되면 특정 컨슈머는 파티션이 할당되지 않아 스레드만 차지하게 되기 때문입니다. 컨슈머 그룹은 다른 컨슈머 그룹과 격리되는 특징을 가지고 있어 컨슈머 그룹끼리 영향을 받지 않게 처리할 수 있으므로 컨슈머 그룹으로 따로 나눌 수 있는 경우는 최대한 나누는 것이 권장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;offset&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbtNzM/btsEEI7U8tH/CKugpx9iUp0vyCybITrrVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbtNzM/btsEEI7U8tH/CKugpx9iUp0vyCybITrrVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbtNzM/btsEEI7U8tH/CKugpx9iUp0vyCybITrrVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbtNzM%2FbtsEEI7U8tH%2FCKugpx9iUp0vyCybITrrVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1552&quot; height=&quot;918&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;컨슈머는 카프카 브로커로부터 데이터를 어디까지 가져갔는지 커밋을 통해 기록합니다. 특정 토픽의 파티션을 어떤 컨슈머 그룹이 몇 번째까지 가져갔는지 카프카 브로커 내부에서 사용되는 내부 토픽(__consumer_offsets)에 기록됩니다. 컨슈머 동작에 이슈가 발생하여 consumer_offsets 토픽에 어느 레코드까지 읽어갔는지 오프셋 커밋이 기록되지 못했다면 데이터 처리의 중복이 발생할 수 있습니다. 데이터 처리의 중복이 발생하지 않게 하기 위해서는 컨슈머 애플리케이션이 오프셋 커밋을 정상적으로 처리했는지 검증해야만 합니다. 오프셋 커밋은 컨슈머 애플리케이션에서 명시적, 비명시적으로 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFgux6/btsEGjF3EwS/CgERevUKxUlg5RPAZgqd1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFgux6/btsEGjF3EwS/CgERevUKxUlg5RPAZgqd1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFgux6/btsEGjF3EwS/CgERevUKxUlg5RPAZgqd1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFgux6%2FbtsEGjF3EwS%2FCgERevUKxUlg5RPAZgqd1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2550&quot; height=&quot;894&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;894&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;기본적으로 컨슈머는 레코드를 처리하는 poll 메서드가 수행될 때 일정 간격마다 오프셋을 커밋하도록(enable.auto.commit=true)로 설정되어 있습니다. 이렇게 일정 간격마다 자동으로 커밋되는 것을 &lt;b&gt;비명시 오프셋 커밋&lt;/b&gt; 이라고 합니다. 이 옵션은 auto.commit-interval.ms에 설정된 값과 함께 사용되는데, poll 메서드가 auto.commit-interval.ms에 설정된 값 이상이 지났을 때 그 시점까지 레코드 오프셋을 커밋합니다. 따라서 명시적으로 커밋 관련 코드를 작성할 필요가 없습니다. 비명시 오프셋 커밋은 편리하지만 poll 메서드 호출 이후에 리밸런싱 또는 컨슈머 강제종료 발생 시 컨슈머가 처리하는 데이터가 중복 또는 유실될 수 있는 가능성이 있는 취약한 구조를 갖고 있습니다. 그러므로 데이터 중복이나 유실을 허용하지 않는 서비스라면 자동 커밋을 사용하면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적으로 오프셋을 커밋하려면 poll 메서드 호출 이후에 반환받은 데이터의 처리가 완료되고 commitSync 메서드를 호출하면 됩니다. commitSync 메서드는 poll 메서드를 통해 반환된 레코드의 가장 마지막 오프셋을 기준으로 커밋을 수행합니다. commitSync 메서드는 브로커에 커밋 요청을 하고 커밋이 정상적으로 처리되었는지 응답하기까지 기다리는데 이는 컨슈머의 처리량에 영향을 끼칩니다. 데이터 처리 시간에 비해 커밋 요청 및 응답에 시간이 오래 걸린다면 동일 시간당 데이터 처리량이 줄어들기 때문입니다. 이를 해결하기 위해 commitAsync 메서드를 사용해 커밋 요청을 전송하고 응답이 오기 전까지 데이터 처리를 수행할 수 있습니다. 하지만 비동기 커밋은 커밋 요청이 실패했을 경우 현재 처리 중인 데이터의 순서를 보장하지 않으며 데이터의 중복 처리가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 카프카 컨슈머에서는 어떤 방식을 사용하든 중복이 발생할 수 있습니다. auto commit은 자동으로 커밋하기 전에 consumer가 죽으면 다른 컨슈머가 중복으로 해당 데이터를 처리할 수 있고, 명시적 커밋도 처리 과정에서 예외가 발생하면 마지막 커밋된 오프셋부터 메시지를 다시 가져오기 때문입니다. 즉, 카프카는 메시지가 중복은 있지만 손실은 없음을 보장합니다. 따라서 가능하다면 파티션별로 메시지 순서 의존성이 없도록 설계하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;poll 동작&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbMZCK/btsEFNgpRmy/3Bh2uPKQPmaYTkGCTwsJrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbMZCK/btsEFNgpRmy/3Bh2uPKQPmaYTkGCTwsJrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbMZCK/btsEFNgpRmy/3Bh2uPKQPmaYTkGCTwsJrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbMZCK%2FbtsEFNgpRmy%2F3Bh2uPKQPmaYTkGCTwsJrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1898&quot; height=&quot;832&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;832&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;컨슈머는 poll 메서드를 통해 레코드들을 반환받지만 poll 메서드를 호출하는 시점에 클러스터에서 데이터를 가져오는 것은 아닙니다. 컨슈머 애플리케이션을 실행하게 되면 내부에서 Fetcher 인스턴스가 생성되어 poll 메서드를 호출하기 전에 미리 레코드들을 내부 큐로 가져옵니다. 이후에 사용자가 명시적으로 poll을 호출하면 컨슈머는 내부 큐에 있는 레코드들을 반환받아 처리를 수행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨슈머 주요 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bootstrap.servers
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서가 데이터를 전송할 대상 카프카 클러스터에 속한 브로커 '호스트 이름:포트' 를 1개 이상 작성&lt;/li&gt;
&lt;li&gt;2개 이상 입력하여 일부 브로커에 이슈가 발생하더라도 접속하는 데에 이슈가 없도록 설정 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;key.deserializer, value.deserializer : 역직렬화 클래스 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선택 옵션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;group.id
&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;/li&gt;
&lt;li&gt;auto.offset.reset
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머 그룹이 특정 파티션을 읽을 때 저장된 컨슈머 오프셋이 없는 경우 어느 오프셋부터 읽을지 선택하는 옵션&lt;/li&gt;
&lt;li&gt;이미 컨슈머 오프셋이 있다면 이 옵션값은 무시됩니다.&lt;/li&gt;
&lt;li&gt;lastest, earliest, none 옵션이 있고 기본값은 latest입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lastest : 가장 마지막(최근)의 오프셋부터 읽기&lt;/li&gt;
&lt;li&gt;earliset : 가장 오래전에 넣은(가장 초기) 오프셋부터 읽기&lt;/li&gt;
&lt;li&gt;none : 컨슈머 그룹이 커밋한 기록이 없으면 오류를 반환하고 있다면 기존 커밋 기록 이후부터 오프셋을 읽기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fetch.min.bytes
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한번에 가져올 수 있는 최소 데이터 사이즈&lt;/li&gt;
&lt;li&gt;만약 지정한 사이즈보다 작은 경우, 요청에 대해 응답하지 않고 데이터가 누적될 때까지 기다립니다.&lt;/li&gt;
&lt;li&gt;무작정 기다리는 것은 아니고 fetch.wait.max.ms 설정의 영향을 받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fetch.wait.max.ms
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브로커에 fetch.min.bytes 이상의 메시지가 쌓일 때까지 최대 대기 시간&lt;/li&gt;
&lt;li&gt;기본은 500ms&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fetch.max.bytes
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한번에 가져올 수 있는 최대 데이터 사이즈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;request.timeout.ms
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청에 대해 응답을 기다리는 최대 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;enable.auto.commit
&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;기본값 true&lt;/li&gt;
&lt;li&gt;auto.commit.interval.ms를 5ms로 설정했다면(기본값) 컨슈머는 poll을 호출하고 5ms후 레코드의 가장 마지막 오프셋을 커밋합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;auto.commit.interval.ms
&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;기본값 5000(5초)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;max.poll.records
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;poll 메서드를 통해 반환되는 레코드 개수 지정&lt;/li&gt;
&lt;li&gt;기본값 500&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;session.timeout.ms
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머가 브로커와 연결이 끊기는 최대 시간&lt;/li&gt;
&lt;li&gt;이 시간 내에 하트비트를 전송하지 않으면 브로커는 컨슈머에 이슈가 발생했다고 가정하고 리밸런싱을 시작합니다.&lt;/li&gt;
&lt;li&gt;보통 하트비트 시간 간격의 3배로 설정합니다.&lt;/li&gt;
&lt;li&gt;기본값은 10000(10초)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;heartbeat.interval.ms
&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;기본값은 3000(3초)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;max.poll.records
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 호출 poll에 대한 최대 레코드 수를 조정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;max.poll.interval.ms
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;poll 메서드를 호출하는 간격의 최대 시간&lt;/li&gt;
&lt;li&gt;hearbeat는 주기적으로 보내고 있으나 실제로 메시지를 가져가지 않는 경우가 있을 수 있어, 컨슈머가 무한정 해당 파티션을 점유할 수 없도록 주기적으로 poll을 호출하지 않으면 장애라고 판단합니다.&lt;/li&gt;
&lt;li&gt;poll 메서드를 호출한 이후에 데이터를 처리하는 데에 시간이 너무 많이 걸리는 경우 비정상으로 판단하고 리밸런싱을 시작합니다.&lt;/li&gt;
&lt;li&gt;기본값은 300000(5분)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;isolation.level
&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;read_committed, read_uncommitted로 설정 가능&lt;/li&gt;
&lt;li&gt;전자는 설정하면 커밋이 완료된 레코드만 읽고 후자는 모든 레코드를 읽습니다.&lt;/li&gt;
&lt;li&gt;기본값은 read_uncommitted&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;partition.assignment.strategy
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션을 컨슈머에 할당할 때 어떤 전략을 선택할 것인지 지정하는 옵션&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partition.assignment.strategy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션을 컨슈머에 할당할 때 어떤 전략을 선택할 것인지 지정하는 옵션입니다. 해당 옵션을 이해하기 위해서는 Consumer Rebalancing protocol 모드를 이해해야 합니다. 리밸런싱은 다음과 같은 상황에서 발생합니다.&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;토픽에 새로운 파티션 추가&lt;/li&gt;
&lt;li&gt;컨슈머 그룹 내에 새로운 컨슈머 추가 or 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Consumer Rebalancing protocol 모드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rebalancing protocol 모드는 Eager과 Cooperative이 있습니다.&lt;/p&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;Eager&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DSoJM/btsEHJxtRrM/AtJ0ApK9RbKTP6SonZPQoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DSoJM/btsEHJxtRrM/AtJ0ApK9RbKTP6SonZPQoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DSoJM/btsEHJxtRrM/AtJ0ApK9RbKTP6SonZPQoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDSoJM%2FbtsEHJxtRrM%2FAtJ0ApK9RbKTP6SonZPQoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2224&quot; height=&quot;408&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리밸런싱 시 기존 컨슈머들의 모든 파티션 할당을 취소하고 재할당 전까지 메시지를 읽지 않습니다.&lt;/li&gt;
&lt;li&gt;모든 컨슈머가 메시지를 재할당 전까지 읽지 않으므로 lag이 발생할 가능성이 높습니다.&lt;/li&gt;
&lt;li&gt;네이밍에 cooperative가 붙어있지 않는 한 Eager이 기본값입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cooperative&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2206&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zLC0Y/btsEEYJuyM6/XpBpGvXSW7SFu1oWl0v8gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zLC0Y/btsEEYJuyM6/XpBpGvXSW7SFu1oWl0v8gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zLC0Y/btsEEYJuyM6/XpBpGvXSW7SFu1oWl0v8gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzLC0Y%2FbtsEEYJuyM6%2FXpBpGvXSW7SFu1oWl0v8gk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2206&quot; height=&quot;480&quot; data-origin-width=&quot;2206&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리밸런싱 시 기존 컨슈머들의 모든 파티션 할당을 취소하지 않고 대상이 되는 컨슈머들에 대해서 파티션에 따라 점진적으로 컨슈머를 할당하면서 리밸런싱을 수행합니다.&lt;/li&gt;
&lt;li&gt;전체 컨슈머가 메시지 읽기를 중지하지 않으며 개별 컨슈머가 협력적으로 영향받는 파티션만 리밸런스로 재분배합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Assigner&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RoundRobinAssignor와 RangeAssigner&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj0d6B/btsEE3qqjxt/yAR97e7kE0nAjY7bpxwtZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj0d6B/btsEE3qqjxt/yAR97e7kE0nAjY7bpxwtZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj0d6B/btsEE3qqjxt/yAR97e7kE0nAjY7bpxwtZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj0d6B%2FbtsEE3qqjxt%2FyAR97e7kE0nAjY7bpxwtZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2548&quot; height=&quot;838&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;라운드 로빈 방식은 순차적으로 파티션을 할당하는 방식이고, range 방식은 다른 토픽이라도 파티션 번호가 같으면 같은 컨슈머에 할당하는 방식입니다. RangeAssignor가 기본값입니다.&lt;/p&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;StickyAssignor&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mIirH/btsEGPY0zYY/skPZ3ZXo9NHHwe5uu98rm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mIirH/btsEGPY0zYY/skPZ3ZXo9NHHwe5uu98rm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mIirH/btsEGPY0zYY/skPZ3ZXo9NHHwe5uu98rm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmIirH%2FbtsEGPY0zYY%2FskPZ3ZXo9NHHwe5uu98rm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2552&quot; height=&quot;938&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sticky방식은 라운드 로빈 방식과 같으나 리밸런싱할 때, Eager로 다 떨어지고 재분배 시 이전을 기억하고 있기 때문에 기존에 붙어있는 것은 그대로 가고 사라진 컨슈머에 매핑되어 있던 것만 round robin으로 동작합니다.&lt;/p&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;CoorperativeStickyAssignor&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TovcD/btsEHo1eOvX/Y1qB9e5PgnLxGR9ORKEAik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TovcD/btsEHo1eOvX/Y1qB9e5PgnLxGR9ORKEAik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TovcD/btsEHo1eOvX/Y1qB9e5PgnLxGR9ORKEAik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTovcD%2FbtsEHo1eOvX%2FY1qB9e5PgnLxGR9ORKEAik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2224&quot; height=&quot;872&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 라운드 로빈으로 동작하고 리밸런싱할 때, Eager가 아니라 계속 붙어있고 죽은 컨슈머에 붙어있던 파티션만 Round Robin방식으로 붙습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리밸런싱&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2504&quot; data-origin-height=&quot;1276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NSu5e/btsEFc1LroK/4WBgIwUFjKNh3xyoHcb1kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NSu5e/btsEFc1LroK/4WBgIwUFjKNh3xyoHcb1kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NSu5e/btsEFc1LroK/4WBgIwUFjKNh3xyoHcb1kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNSu5e%2FbtsEFc1LroK%2F4WBgIwUFjKNh3xyoHcb1kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2504&quot; height=&quot;1276&quot; data-origin-width=&quot;2504&quot; data-origin-height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;컨슈머 그룹으로 이뤄진 컨슈머들 중 일부 컨슈머에 장애가 발생하면, 장애가 발생한 컨슈머에 할당된 파티션은 장애가 발생하지 않은 컨슈머로 소유권이 넘어가는데 이를 &lt;b&gt;리밸런싱이라고&lt;/b&gt; 합니다. 리밸런싱은 크게 컨슈머가 추가되는 상황, 컨슈머가 제외되는 상황 두 가지에서 발생합니다. 이는 카프카 브로커 중 한대가 그룹 조정자의 역할을 수행하는데 그룹 조정자(group coordinator)는 컨슈머 그룹의 컨슈머가 추가되고 삭제될 때를 감지하여 리밸런싱을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 장애 상황이 아니라면 배포하는 시점에 기존 애플리케이션이 종료되고 새로운 애플리케이션이 추가되면서 리밸런싱이 발생합니다. 애플리케이션은 종료되는 시점에 kafkaConsumer.close 메서드를 호출하면서 코디네이터에게 컨슈머가 종료되었다는 사실을 알리고 코디네이터는 이를 통해 파티션 리밸런싱을 수행합니다. 만약 컨슈머가 종료되면서 close 메서드를 호출하지 않는다고 해도 컨슈머는 heartbeat.interval.ms 간격으로 코디네이터에게 하트비트를 보내게 되는데 이를 받지 못하면서 해당 컨슈머의 장애 및 종료 상태를 인지하고 컨슈머 그룹에서 제외시키면서 리밸런싱을 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 보면 리밸런싱이 간단해보이나 리밸런싱에도 단점이 있습니다. 기본 설정이라면 리밸런싱이 일어나는 동안 일시적으로 컨슈머는 메시지를 가져갈 수 없습니다. 그래서 파티션 리밸런싱이 발생하면 컨슈머 그룹 전체의 메시지 처리가 멈추는 현상이 발생합니다. 리밸런싱이 일어나는 과정을 하나의 예시로 알아봅시다. 컨슈머 애플리케이션이 종료되는 과정에서 kafkaConsumer.close메서드를 호출해서 코디네이터가 해당 컨슈머를 그룹에서 제외하고 리밸런싱이 일어난다고 가정해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uvahg/btsEGjTz1jo/r9Vicc91A1khvcd5KSO2P0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uvahg/btsEGjTz1jo/r9Vicc91A1khvcd5KSO2P0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uvahg/btsEGjTz1jo/r9Vicc91A1khvcd5KSO2P0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuvahg%2FbtsEGjTz1jo%2Fr9Vicc91A1khvcd5KSO2P0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;537&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;537&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;그룹 내 남아있는 컨슈머들은 poll 메서드 호출 시 그룹에 다시 조인해야 하는지를 확인합니다. 컨슈머 그룹에 새로운 컨슈머가 추가되었거나, 기존 컨슈머가 그룹에서 제외된 경우에 컨슈머들은 그룹에 다시 조인해야 합니다. 컨슈머가 그룹에 다시 조인해야 할 때에는 코디네이터에게 조인 요청을 보냅니다. 컨슈머는 조인 요청을 보낼 때 메타데이터를 추가해서 보낼 수 있습니다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 과정은(1~4) KafkaConsumer.poll 메서드 내부에서 진행됩니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 과정을 통해서 알 수 있듯이 파티션 리밸런싱이 일어났을 때, 파티션을 그룹 내 컨슈머들에게 어떻게 분배할지는 그룹 내 리더가 결정합니다. 코디네이터는 컨슈머 그룹 내의 리더를 선출하고, 리더가 보낸 새로운 파티션 할당 정보를 팔로워에게 분배하는 정도의 역할만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 주목할 만 사실은 &lt;b&gt;코디네이터는 그룹 내의 모든 컨슈머들로부터 조인 요청을 받은 이후에 리더를 선출한다&lt;/b&gt;는 것입니다. 따라서 그룹 내의 모든 컨슈머들이 poll 메서드를 호출해야지만 파티션 리밸런싱이 완료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그룹 내 특정 컨슈머가 poll 메소드를 호출하지 않은 경우&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 내 모든 컨슈머들이 조인 요청을 보내야만 코디네이터가 리더를 선출한다고 했습니다. 특정 컨슈머가 poll 메서드를 호출하지 못하고 있어서 조인 요청을 보내지 못한다고 해서 리더 선출을 못하는 것은 아닙니다. 컨슈머의 조인 요청에는 rebalanceTimeout을 포함하고 있습니다. 컨슈머들은 파티션 리밸런싱이 시작된 이후에 rebalanceTimeout 시간 내에 조인 요청을 보내야 하고 만약 컨슈머가 rebalanceTimeout 이내에 조인 요청을 보내지 못하는 경우 그룹에서 제외됩니다.(rebalanceTimeout은 max.poll.interval.ms 값으로 세팅됩니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카프카 컨슈머 애플리케이션의 일반적인 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Spring Kafka 애플리케이션은 poll 메서드를 호출해서 레코드(메시지)를 가져오고 동일한 스레드에서 레코드를 처리합니다. 그리고 가져온 레코드를 전부 처리한 후에 다시 poll 메소드를 호출해서 새로운 레코드를 가져옵니다. poll 메소드를 통해 한 번에 가져올 수 있는 최대 레코드 수는 max.poll.records 속성으로 정해지며 기본값은 500입니다. 기본값을 사용하는 경우 poll 메소드를 통해 레코드를 최대 500개까지 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 특징을 가진 컨슈머 애플리케이션이 있다고 가정해 보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;max.poll.records 속성이 500(기본값)으로 세팅됨&lt;/li&gt;
&lt;li&gt;하나의 레코드를 처리하는데 약 0.5초가 걸림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 특징을 가진 컨슈머 애플리케이션의 poll 메서드 호출 간격을 계산해 보면 다음과 같습니다. poll 메서드를 통해 500개의 레코드를 가져오고 하나의 레코드를 처리하는데 약 0.5초 걸린다면, 500개의 레코드를 처리하는 데는 약 250(500 * 0.5)초가 걸립니다. poll 메소드를 통해 가져온 레코드를 전부 처리한 후에 다시 poll을 호출하는 일반적인 구조라면 poll 메서드의 호출 간격이 약 250초입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머가 계속해서 하트비트만 보내고, 실제로 메시지를 가져가지 않는 경우가 있을 수도 있습니다. 이러면 컨슈머가 메시지를 처리하지 않고 해당 파티션을 무한정 점유할 수 있습니다. 이를 방지하기 위한 설정은 max.poll.interval.ms입니다. 컨슈머가 이 시간 동안 poll을 호출하지 않으면, 코디네이터는 해당 컨슈머가 장애라고 판단하고 컨슈머 그룹에서 제외하고, 파티션 리밸런싱이 일어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포 시 고려사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;poll 메서드의 호출 간격이 길다면, 애플리케이션 배포 시 고려해야 할 사항이 있습니다. 위에서 설명했듯이 리밸런싱 과정은 그룹 내 모든 컨슈머들이 poll 메소드를 통해 조인 요청을 코디네이터에게 보내야만 완료됩니다. 따라서 poll 메소드의 호출 간격이 길어지면 그만큼 파티션 리밸런싱이 완료되는 데까지 걸리는 시간이 길어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6wt4y/btsEGRJifKa/a65jsNIveBdiYgMnFc5S8K/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6wt4y/btsEGRJifKa/a65jsNIveBdiYgMnFc5S8K/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6wt4y/btsEGRJifKa/a65jsNIveBdiYgMnFc5S8K/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6wt4y%2FbtsEGRJifKa%2Fa65jsNIveBdiYgMnFc5S8K%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;796&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;796&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;그룹 내의 컨슈머가 3개 존재하며, poll 메소드의 호출 간격이 250초라고 가정해 보겠습니다. 그리고 한 개의 컨슈머가 정상적으로 종료되었습니다.&lt;/li&gt;
&lt;li&gt;기존 컨슈머가 그룹에서 제거되었기 때문에 파티션 리밸런싱이 일어납니다. 컨슈머 B는 컨슈머 A가 그룹에서 제외된 후 얼마 지나지 않아서 poll 메서드를 호출했습니다. 컨슈머 B는 poll 메소드에서 코디네이터에게 조인 요청을 보냅니다. 그리고 코디네이터가 리더를 선출해서 응답을 줄 때까지 기다립니다.&lt;/li&gt;
&lt;li&gt;컨슈머 C는 이전 poll을 통해 가져온 레코드를 전부 처리하지 못했기 때문에 poll 메소드를 아직 호출하지 못하고 있습니다. 코디네이터는 컨슈머 C로부터 조인 요청을 받지 못했기 때문에 리더를 선출할 수 없습니다. 그리고 컨슈머 B는 리더가 선출되기를 기다리고 있습니다.&lt;/li&gt;
&lt;li&gt;컨슈머 C가 약 150초 정도 지난 뒤에 poll 메소드를 호출했다고 가정해 보겠습니다. 코디네이터는 그룹 내 모든 컨슈머로부터 조인 요청을 받았기 때문에 그룹 내 리더를 선출합니다. 그리고 코디네이터는 컨슈머 B와 컨슈머 C에게 조인 요청에 대한 응답을 보냅니다. 응답에는 어떤 컨슈머가 그룹 내 리더인지 알 수 있는 정보가 포함되어 있습니다.&lt;/li&gt;
&lt;li&gt;리더는 컨슈머들의 메타데이터 등을 참고해서 각 컨슈머에게 파티션을 어떻게 할당할지를 결정합니다. 그리고 결정된 사항을 코디네이터에게 전달합니다. 코디네이터는 새로운 파티션 할당 정보를 팔로워에게 전달함으로써 파티션 리밸런싱이 완료됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상황은 poll 메서드의 호출주기가 길다면 충분히 발생할 수 있습니다. 애플리케이션의 특징에 따라 파티션 리밸런싱 시간이 중요하지 않을 수 있습니다. &lt;b&gt;하지만 주문처리 혹은 결제와 같은 준 실시간 작업을 카프카를 이용해서 비동기로 처리하고 있다면 파티션 리밸런싱 시간을 단축하는 것이 좋습니다.&lt;/b&gt; 파티션 리밸런싱이 일어나는 동안 컨슈머들이 레코드를 가져오지 못하기 때문에, 파티션 리밸런싱 시간이 오래 걸릴수록 LAG이 커질 수 있습니다. 이로 인해 준 실시간으로 처리되어야 하는 시스템에서는 메시지 처리 지연이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 리밸런싱 시간을 줄이기 위해서는 poll 메소드의 호출 간격을 줄여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방안 1. 스레드의 분리&lt;/b&gt;&lt;br /&gt;파티션 리밸런싱 시간을 줄이기 위해 고려해 볼 수 있는 첫 번째 방법으로는 컨슈머 스레드와 레코드를 처리하는 스레드를 분리하는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ngpJO/btsEEAh11GQ/3GXl1TWtMLdAATJLlZydVk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ngpJO/btsEEAh11GQ/3GXl1TWtMLdAATJLlZydVk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ngpJO/btsEEAh11GQ/3GXl1TWtMLdAATJLlZydVk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FngpJO%2FbtsEEAh11GQ%2F3GXl1TWtMLdAATJLlZydVk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1352&quot; height=&quot;540&quot; data-origin-width=&quot;1352&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;스레드를 분리하는 방법으로 스레드 풀을 사용하는 것을 고려해 볼 수 있습니다. 위와 같이 컨슈머 스레드에서는 레코드만 가져오고, 레코드에 대한 처리는 스레드 풀에서 합니다. 컨슈머 스레드는 레코드가 처리되기까지 기다릴 필요가 없으므로, 아주 짧은 간격으로 poll 메서드를 호출할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 구조의 가장 큰 단점은 같은 파티션에 있는 레코드들의 처리 순서가 바뀔 수 있다는 것입니다. 예를 들어 0번 파티션에 3개의 레코드가 있다고 가정해 보겠습니다. 그리고 0번 파티션을 소비하고 있는 컨슈머가 3개의 레코드를 가져오고 레코드에 대한 처리는 별도의 스레드 풀에서 합니다. 이때 사용 가능한 스레드의 개수가 3개 이상이라면, 3개의 레코드는 거의 동시에 처리되기 시작할 것입니다. 이런 경우 3개의 레코드 중에서 어떤 레코드가 가장 먼저 처리될지는 알 수 없으며 결과적으로 레코드 처리 순서가 보장되지 않습니다. 물론 처리 순서가 중요하지 않은 시스템에서는 이 부분이 문제가 되지 않지만, 같은 파티션에 존재하는 레코드들의 처리 순서가 매우 중요하다면 위와 같은 구조를 사용하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 스레드 풀 크기를 신중하게 고려해야 합니다. 스레드 풀의 크기가 너무 작으면 버퍼가 무한정 커지게 되며 이는 장애를 일으킬 수 있습니다. 반면에 스레드 풀의 크기가 불필요하게 크다면 메모리가 낭비될 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀을 사용하더라도 컨슈머가 메시지를 가져오는 속도와 메시지를 처리하는 속도 간에 차이가 발생할 수 있습니다. 컨슈머가 메시지를 가져오는 속도가 더 빠른 애플리케이션의 경우 스레드 풀의 버퍼가 가득 찰 수 있습니다. 버퍼가 가득 찬 상태에서 스레드 풀이 추가적인 요청을 받으면 예외가 발생합니다. 또한, 메시지 사이즈가 큰 경우, 많은 메시지가 버퍼에 저장되면 Out of Memory 에러가 발생할 수도 있습니다. 따라서 이러한 속도 차이가 존재하는 경우, 백프레셔 구현을 고려해봐야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방안 2. max.poll.records 속성 변경&lt;/b&gt;&lt;br /&gt;이번에는 레코드 처리를 별도의 스레드에서 하지 않는 단일 스레드 구조에서 poll 메서드 호출 간격을 줄이는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 스레드 구조에서 poll 메서드의 호출 간격은 (레코드 하나를 처리하는 데 걸리는 시간) X (poll 메서드를 통해 가져온 레코드의 수) 입니다. 따라서 poll 메소드의 호출 간격을 줄이기 위해서는 레코드 하나를 처리하는데 걸리는 시간을 줄이거나, 한 번의 poll 메소드를 통해 가져오는 레코드의 수를 줄여야 합니다. 하지만 레코드를 처리하는 과정에서 외부 시스템에 의존하거나, DB를 조회하는 등 여러 가지 작업이 일어나는 경우 레코드 처리 시간을 줄이기는 어렵습니다. 반면에 poll 메소드를 통해 가져오는 레코드의 수를 줄이기는 매우 쉽습니다. 컨슈머의 max.poll.records 속성을 변경하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max.poll.records 속성을 5로 설정했다고 가정해 보겠습니다. 그러면 poll 메서드 호출을 통해 최대 5개의 레코드를 가져올 수 있습니다. 하나의 레코드를 처리하는데 걸리는 시간이 0.5초라고 한다면 poll 메소드를 통해 가져온 모든 레코드를 처리하는데 걸리는 시간은 약 2.5초가 됩니다. 일반적은 컨슈머 애플리케이션은 poll 메소드 호출을 통해 가져온 레코드를 모두 처리한 후에 다음 poll을 호출합니다. 따라서 poll 메소드 호출주기가 약 2.5초가 됩니다. poll 메서드의 호출 주기가 짧아진 만큼 파티션 리밸런싱이 완료되기까지 걸리는 시간도 짧아집니다. 이런 경우라면 보통 5초 이내에 파티션 리밸런싱 작업이 완료될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max.poll.records를 작게 설정하면 성능이 크게 떨어질 거로 생각할 수 있습니다. 하지만 실제로 &lt;b&gt;max.poll.records를 작게 설정하더라고 성능에 큰 영향을 주지는 않습니다.&lt;/b&gt;&amp;nbsp;그 이유는 컨슈머가 레코드를 가져올 때 Fetcher라는 클래스를 사용하기 때문입니다. 실제로 poll 메서드에서 레코드를 가져오는 부분은 아래와 같은 순서로 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kF8e5/btsEHpeMnDd/UE0fpbzAzIlnc2KSJZ0aG0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kF8e5/btsEHpeMnDd/UE0fpbzAzIlnc2KSJZ0aG0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kF8e5/btsEHpeMnDd/UE0fpbzAzIlnc2KSJZ0aG0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkF8e5%2FbtsEHpeMnDd%2FUE0fpbzAzIlnc2KSJZ0aG0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;583&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;583&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컨슈머는 poll 메서드가 호출되면, Fetcher의 fetchedRecords 메서드를 호출합니다. fetchedRecords 메소드는 최대 max.poll.records 만큼의 레코드를 리턴합니다.&lt;/li&gt;
&lt;li&gt;만약에 Fetcher가 레코드를 가지고 있지 않다면, fetchedRecords 메소드는 빈 Map을 반환합니다. 그리고 빈 Map이 반환된 경우에만 컨슈머는 Fetcher#sendFetches 메서드를 호출합니다. sendFetches 메소드에서는 Fetcher가 브로커에게 요청을 보내 레코드를 가져옵니다. 하나의 요청으로 최대 fetch.max.bytes 크기만큼 가져올 수 있고, 파티션당 최대 max.partition.fetch.bytes 크기만큼 가져올 수 있습니다. 그리고 요청은 현재 컨슘하고 있는 파티션의 리더에게 모두 보냅니다. 예를 들어, 현재 컨슈머가 컨슘하고 있는 파티션이 0, 1이고 0번 파티션의 리더가 브로커 1, 1번 파티션의 리더가 브로커 2라면, 컨슈머는 브로커 1, 2에게 각각 요청을 보냅니다. 즉 2개의 요청을 보내게 됩니다.&lt;/li&gt;
&lt;li&gt;Fetcher#sendFetches 메소드를 호출 후, 컨슈머는 또다시 fetchedRecords 메소드를 호출합니다. 그러면 Fetcher는 요청을 통해 가져온 레코드 중에서 최대 max.poll.records 만큼의 레코드를 반환합니다.&lt;/li&gt;
&lt;li&gt;컨슈머의 poll 메서드가 또다시 호출이 된 경우, 컨슈머는 Fetcher#fetchedRecords 메소드를 호출할 것입니다. 이전 요청을 통해 가져온 레코드가 아직 남아있다면, Fetcher는 브로커에게 요청을 보내지 않고 가지고 있는 레코드 중에서 max.poll.records 만큼의 레코드를 바로 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과적으로 Fetcher는 가지고 있는 레코드가 없는 경우에만 브로커에게 요청을 보냅니다.&lt;/b&gt; 따라서 max.poll.records를 작게 설정해도 성능에 큰 영향을 주진 않습니다. 오히려 fetch.max.bytes 설정과 max.partition.fetch.bytes 설정이 성능에 큰 영향을 줍니다. max.poll.records 속성을 작게 설정했음에도 하나의 레코드를 처리하는데 너무 오랜 시간이 걸린다면 poll 메소드 호출 간격이 길어질 수밖에 없습니다. 그러면 배포 시에 파티션 리밸런싱 시간이 길어질 수 있다는 한계가 존재합니다. 이런 경우는 레코드를 별도의 스레드 풀에서 처리하는 모델을 고려해봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 애플리케이션을 배포하는 경우 파티션 리밸런싱이 일어납니다. 파티션 리밸런싱은 그룹 내 모든 컨슈머들이 poll 메소드를 호출해야지만 완료됩니다. 따라서 poll 메소드 호출간격이 긴 경우에 파티션 리밸런싱이 오래 걸릴 수 있습니다. 파티션 리밸런싱이 일어나는 동안 컨슈머는 레코드를 가져오지 못하고, 리밸런싱이 끝나기를 기다리기 때문에 파티션 리밸런싱이 LAG을 유발할 수 있습니다. 실시간 처리가 중요하지 않는 경우에는 크게 문제가 되지 않지만 실시간 처리가 중요한 시스템에서는 문제가 될 수 있습니다. 따라서 실시간 처리가 중요하다면 max.poll.records 속성을 충분히 작게 설정하는 것을 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;튜닝 파라미터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Fetch Request&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetch.min.bytes&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetcher가 record들을 읽어들이는 최소 바이트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;브로커는 지정된 만큼 새로운 메시지가 쌓일때 까지 전송하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 1바이트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;무작정 기다리진 않고 아래 옵션에서 최대 대기 시간이 존재한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;max.partition.fetch.bytes&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetcher가 파티션별 한번에 최대로 가져올 수 있는 바이트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 1MB&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;파티션이 10개 있다면 10MB를 가져오는 것인데 무한적 가져올 순 없고 fetch.max.bytes에 제약을 받는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetch.max.bytes&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetcher가 한번에 가져올 수 있는 최대 데이터 바이트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 50mb&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetch.wait.max.ms&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;브로커에 fetch.min.bytes 이상의 메시지가 쌓일때까지 최대 대기 시간&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본은 500ms&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;receive.buffer.bytes&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;데이터를 읽을 때 사용하는 TCP 수신 버퍼 크기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본값 64kb&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;-1이면 OS 기본값을 사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Offset auto commit&lt;/span&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;enable.auto.commit&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;true면 컨슈머의 오프셋을 백그라운드에서 주기적으로 커밋한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 true&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;auto.commit.interval.ms를 5ms로 설정했다면(기본값) 컨슈머는 poll을 호출하고 5ms 후 가장 마지막 오프셋을 커밋한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;auto.commit.interval.ms&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;자동 커밋 주기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 5000ms(5초)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Consumer group의 consumer 수 조정(application scale up 또는 out)&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;max.poll.records&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetcher가 한번에 가져올 수 있는 레코드 수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 500&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;max.poll.interval.ms&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이전 poll 호출 후 다음 poll까지 브로커가 기다리는 시간&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 300000ms&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://medium.com/11st-pe-techblog/%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%BB%A8%EC%8A%88%EB%A8%B8-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-%EC%A0%84%EB%9E%B5-4cb2c7550a72&quot;&gt;카프카 컨슈머 애플리케이션 배포 전략&lt;/a&gt;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>consumer</category>
      <category>Kafka</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/77</guid>
      <comments>https://backtony.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 9 Feb 2024 19:04:42 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - Producer</title>
      <link>https://backtony.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로듀서 객체 직렬화 전송&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tTyAp/btsEEHOMlJv/ly4UjT5gvsX0AGsCLaYUX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tTyAp/btsEEHOMlJv/ly4UjT5gvsX0AGsCLaYUX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tTyAp/btsEEHOMlJv/ly4UjT5gvsX0AGsCLaYUX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtTyAp%2FbtsEEHOMlJv%2Fly4UjT5gvsX0AGsCLaYUX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1964&quot; height=&quot;754&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서는 보내는 객체를 직렬화화여 바이트 배열 형태로 브로커로 전송합니다. 브로커는 파티션에 바이트 배열 형태로 저장하고 컨슈머는 바이트 배열을 받아서 역직렬화로 객체를 복원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메시지 키값 여부에 따른 전송&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1iksW/btsEEFDoIO0/KZe6IJ9PcV8RRR8KI998Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1iksW/btsEEFDoIO0/KZe6IJ9PcV8RRR8KI998Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1iksW/btsEEFDoIO0/KZe6IJ9PcV8RRR8KI998Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1iksW%2FbtsEEFDoIO0%2FKZe6IJ9PcV8RRR8KI998Z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1624&quot; height=&quot;774&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 키의 사용 여부는 데이터 처리 순서와 관련이 있습니다. 카프카 프로듀서에서 브로커로 메시지를 보낼 때, 메시지 키를 지정할 수 있습니다. 메시지 키를 지정하지 않으면 스티키 파티셔닝(2.4버전 이후)방식으로 특정 파티션으로 전송되는 하나의 배치에 메시지를 빠르게 먼저 채워서 보냅니다. 이 경우에는 전송하는 대상 토픽이 파티션을 여러 개 가지고 있는 경우, &lt;b&gt;메시지의 전송 순서가 보장되지 않은 채로 컨슈머가 읽는 경우가 발생합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 메시지 키를 사용하면 메시지 키를 사용하면 프로듀서가 토픽으로 데이터를 보낼 때 메시지 키를 해시 변환하여 메시지를 파티션에 매칭시킵니다. 따라서 메시지 키가 같다면 항상 같은 파티션으로 메시지가 전송되므로 순서를 보장할 수 있습니다. 하지만 파티션 개수가 달라지면 매칭이 깨지고 다른 파티션에 데이터가 할당되기 때문에 이때부터 컨슈머는 특정 메시지 키의 순서를 보장받지 못합니다. 즉, 메시지 키를 사용하고 처리 순서가 보장되어야 한다면 최대한 파티션의 변화가 발생하지 않는 방식으로 운영해야 합니다. 만약 파티션 개수가 변해야 하는 경우에는 기존에 사용하던 메시지 키의 매칭을 그대로 가져가기 위해 커스텀 파티셔너를 개발하고 적용해야 합니다. 이러한 어려움 때문에 보통 메시지 키별로 처리 순서를 보장하기 위해서는 파티션 개수를 프로듀서가 전송하는 데이터양보다 더 넉넉하게 잡고 생성하는 것이 권장됩니다. 반면에 처리 순서가 관계없다면 처음부터 넉넉하게 잡지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로듀서 내부 배치 전송 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2488&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WNZlC/btsEEBVssyF/0J7JMfKkdN77NXpVCdukp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WNZlC/btsEEBVssyF/0J7JMfKkdN77NXpVCdukp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WNZlC/btsEEBVssyF/0J7JMfKkdN77NXpVCdukp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWNZlC%2FbtsEEBVssyF%2F0J7JMfKkdN77NXpVCdukp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2488&quot; height=&quot;852&quot; data-origin-width=&quot;2488&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProducerRecord -&amp;gt; send 호출 -&amp;gt; Partitioner -&amp;gt; Accumulator 내부에 토픽별로 배치를 만들어 저장 -&amp;gt; sender -&amp;gt; 카프카 클러스터&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서는 카프카 브로커로 데이터를 전송할 때 내부적으로 파티셔너를 통해 배치 생성 단계를 거칩니다. 프로듀서 인스턴스 생성 시 파티셔너를 따로 설정하지 않는다면 기본값인 DefaultPartitioner로 설정되어 전송되는 파티션이 정해집니다. 파티셔너에 의해 구분된 레코드는 데이터를 전송하기 전에 Accumulator에서 데이터를 버퍼로 쌓아놓고 버퍼로 쌓인 데이터는 배치로 묶여서 전송됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로듀서 주요 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bootstrap.servers
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서가 데이터를 전송할 대상 카프카 클러스터에 속한 브로커의 '호스트 이름:포트' 를 1개 이상 작성합니다.&lt;/li&gt;
&lt;li&gt;2개 이상 브로커 정보를 입력하여 일부 브로커에 이슈가 발생하더라도 접속하는 데에 이슈가 없도록 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;key.serializer
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레코드 메시지 키 직렬화 클래스를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;value.serializer
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레코드 메시지 값을 직렬화 클래스를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선택 옵션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;acks
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서가 전송한 데이터가 브로커들에 정상적으로 저장되었는지 전송 성공 여부 확인에 사용하는 옵션으로 0,1,-1(all)로 구성됩니다.&lt;/li&gt;
&lt;li&gt;1(기본값) : 리더 파티션에 데이터가 저장되면 전송 성공으로 판단&lt;/li&gt;
&lt;li&gt;0 : 프로듀서가 전송한 즉시 브로커에 데이터 저장 여부와 상관없이 성공으로 판단&lt;/li&gt;
&lt;li&gt;-1 또는 all : min, insync, replicas 개수에 해당하는 리더 파티션과 팔로워 파티션에 데이터가 저장되면 성공으로 판단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;min.insync.replicas
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;acks옵션과 연관된 옵션을 브로커가 프로듀서로 ack 응답을 보내기 위한 리더가 확인해야 할 최소 replicas 개수를 지정&lt;/li&gt;
&lt;li&gt;replicas가 3개로 구성되어 있고 acks가 all이더라도 이 옵션이 1로 되어있다면 리더만 메시지를 수신해도 조건이 충족되어 ack를 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;buffer.memory
&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;기본값은 32MB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;retries
&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;기본값은 2147483647&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;batch.size
&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;기본값은 16384&lt;/li&gt;
&lt;li&gt;클라이언트에 장애가 발생하면 배치 내 있던 메시지는 전달되지 않기 때문에 고가용성이 필요한 경우라면 배치 사이즈를 주지 않는 것도 방법입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;linger.ms
&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;기본값 0&lt;/li&gt;
&lt;li&gt;배치형태의 메시지를 보내기 전에 추가적인 메시지들을 위해 기다리는 시간으로 배치 사이즈에 도달하면 이 시간과 관계없이 메시지를 즉시 전송하고 배치 사이즈에 도달하지 못한 상황에서 이 시간만큼 도달했을 경우, 메시지를 전송합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;partitioner.class
&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;기본값은 DefaultPartitioner&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;enable.idempotence
&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;기본값은 false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;transaction.id
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서가 레코드를 전송할 때 레코드를 트랜잭션 단위로 묶을지 여부를 설정합니다.&lt;/li&gt;
&lt;li&gt;프로듀서의 고유한 트랜잭션 아이디를 설정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이 값을 설정하면 트랜잭션 프로듀서가 동작하고 기본값은 null입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;max.reqeust.size
&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;기본값은 1MB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;max.in.flight.requests.per.connection
&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;기본값은 5&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISR(In-Sync-Replicas)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR은 리더 파티션과 팔로워 파티션이 모두 싱크 된 상태를 뜻합니다. 예를 들어 복제 개수가 2인 토픽에서 리더 파티션에 오프셋이 [0,3]까지 있고 팔로워 파티션에 [0,2]까지 있으면 동기화가 완벽하게 된 상태가 아닙니다. 팔로워 파티션도 [0,3]까지 있어야 ISR입니다. ISR 용어가 나온 이유는 팔로워 파티션이 리더 파티션으로부터 데이터를 복제하는 데에 시간이 걸리기 때문입니다. 리더 파티션에 데이터가 적재된 이후 팔로워 파티션이 복제하는 시간차 때문에 리더 파티션과 팔로워 파티션 간에 오프셋 차이가 발생하게 되고 이런 차이를 모니터링하기 위해 리더 파티션은 replica.lag.time.max.ms 의 주기를 가지고 팔로워 파티션이 데이터를 복제하는지 확인합니다. 해당 시간보다 더 긴 시간 동안 데이터를 가져가지 않는다면 해당 팔로워 파티션에 문제가 생긴 것으로 판단하고 ISR 그룹에서 제외합니다. ISR로 묶인 파티션은 모두 동일한 데이터가 존재하기 때문에 팔로워 파티션은 리더로 선출될 자격을 가지게 됩니다. 따라서 리더 파티션에 문제가 생기면 ISR 그룹에 속한 파티션 중 하나가 리더 파티션으로 승격됩니다. ISR 이외의 파티션도 리더로 선출 자격을 부여하고 싶다면 unclean.leader.election.enable 값을 true로 주면 됩니다. 다만 false로 설정하면 장애 발생 시 ISR에 남아있는 파티션이 없다면 브로커가 다시 실행될 때까지 서비스가 중단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;acks 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 프로듀서의 acks 옵션은 0, 1, all(또는 -1) 값을 가질 수 있습니다. 이 옵션을 통해 프로듀서가 전송한 데이터가 카프카 클러스터에 얼마나 신뢰성 높게 저장할지 지정할 수 있습니다.&lt;/p&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;acks=0&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4vXb9/btsEFShBnCT/WCLf3KKM3HdAxGeYrmLWKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4vXb9/btsEFShBnCT/WCLf3KKM3HdAxGeYrmLWKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4vXb9/btsEFShBnCT/WCLf3KKM3HdAxGeYrmLWKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4vXb9%2FbtsEFShBnCT%2FWCLf3KKM3HdAxGeYrmLWKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2400&quot; height=&quot;748&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서가 리더 파티션으로 데이터를 전송했을 때 리더 파티션으로 데이터가 저장되었는지 확인하지 않는다는 뜻입니다. 리더 파티션은 데이터가 저장된 이후에 데이터가 몇 번째 오프셋에 저장됐는지 리턴하는데, 이에 대해 응답을 받지 않는다는 의미입니다. 이때는 프로듀서가 전송 하자마자 데이터가 저장되었음을 가정하고 다음으로 넘어가기 때문에 데이터 전송 실패여부를 알 수 없기에 retries 옵션값도 무의미합니다. 덕분에 전송 속도는 가장 빠르고 데이터가 일부 유실되더라도 전송속도가 중요한 경우 사용합니다.&lt;/p&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;acks=1&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2398&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYacii/btsEEyLmGpu/otH04XgXybwdktvk4MOzN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYacii/btsEEyLmGpu/otH04XgXybwdktvk4MOzN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYacii/btsEEyLmGpu/otH04XgXybwdktvk4MOzN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYacii%2FbtsEEyLmGpu%2FotH04XgXybwdktvk4MOzN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2398&quot; height=&quot;720&quot; data-origin-width=&quot;2398&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프로듀서가 보낸 데이터가 리더 파티션에만 정상적으로 적재되었는지 확인합니다. 적재되지 않았다면 재시도할 수 있습니다. 리더 파티션에 적재됐음을 보장하더라도 데이터는 유실될 수 있습니다. 복제 개수가 2이상이라면 팔로워 파티션에 데이터가 복제되기 직전에 리더 파티션에 문제가 발생하면 동기화되지 못한 일부 데이터가 유실되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;acks=all 또는 acks=-1&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjfzRn/btsEFTgt8dB/gm8FzsIgImlUdqWfFNFIrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjfzRn/btsEFTgt8dB/gm8FzsIgImlUdqWfFNFIrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjfzRn/btsEFTgt8dB/gm8FzsIgImlUdqWfFNFIrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjfzRn%2FbtsEFTgt8dB%2Fgm8FzsIgImlUdqWfFNFIrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2386&quot; height=&quot;796&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프로듀서가 보낸 데이터가 리더 파티션과 팔로워 파티션에 모두 정상적으로 적재되었는지 확인합니다. 따라서 일부 브로커에 장애가 발생하더라도 안전하게 전송, 저장을 보장하지만 앞선 옵션에 비해 느립니다. all로 설정한 경우에는 토픽 단위로 설정 가능한 min.insync.replicas 옵션값에 따라 데이터의 안전성이 달라집니다. all은 모든 리더 파티션과 팔로워 파티션의 적재를 뜻하는 것은 아니고 ISR에 포함된 파티션들을 뜻하는 것이기 때문입니다. min.insync.replicas 옵션은 프로듀서가 리더 파티션과 팔로워 파티션에 적재되었는지 확인하기 위한 최소 ISR그룹의 파티션 개수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, min.insync.replicas가 1이라면 ISR 중 최소 1개 이상의 파티션에 데이터가 적재되었음을 확인하는 것입니다. 이 경우 acks를 1로 했을 때와 동일한 동작을 하는데 ISR 중 가장 처음 적재가 완료되는 파티션은 리더 파티션이기 때문입니다. 따라서 min.insync.replicas 를 2 이상으로 설정했을 때부터 acks=all 설정이 의미가 있어집니다. 2로 둔다면 적어도 리더 파티션과 1개의 팔로워 파티션에 데이터가 적재되었음을 보장합니다. min.insync.replicas를 설정할 때는 복제 개수도 함께 고려해야 합니다. 예를 들어 복제 개수를 3으로 설정하고 min.insync.replicas를 3으로 설정하게 된다면, 브로커 1대가 이슈가 발생하게 되면 브로커가 2개만 남는데 min.insync.replicas는 최소한 3개 복제를 보장해야하기 때문에 동작하지 못하게 됩니다. &lt;b&gt;즉, min.insync.replicas옵션은 반드시 파티션 복제본 개수 미만으로 설정해서 운영해야 합니다.&lt;/b&gt; 상용 환경에서는 브로커를 3대 이상으로 묶어서 클러스터를 구성하게 되는데 가장 안정적인 방식은 토픽 복제 개수는 3, min.insync.replicas는 2로 설정하고 프로듀서는 acks=all로 설정하는 것을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전송과 재전송 메커니즘&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;1306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HAFYu/btsEF3iXfz2/vWib6Qs8eSj3y71HE7E4rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HAFYu/btsEF3iXfz2/vWib6Qs8eSj3y71HE7E4rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HAFYu/btsEF3iXfz2/vWib6Qs8eSj3y71HE7E4rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHAFYu%2FbtsEF3iXfz2%2FvWib6Qs8eSj3y71HE7E4rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2200&quot; height=&quot;1306&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;1306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;record accumulator과 꽉 차있는 상태일 경우 send 메시지를 보내지 못할 경우가 있습니다. 이때는 max.block.ms 옵션만큼 기다리게 되고 이후에도 보내지 못하면 timeout exception이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sender Thread는 record Accumulator에서 배치별로 전송하기 위한 최대 대기시간(linger.ms)동안 대기하고 보냅니다. 그리고 브로커로부터 응답을 request.timeout.ms 만큼 기다립니다. request.timeout.ms 초과 시에도 응답이 오지 않는다면 재시도하거나 timeout exception이 발생합니다. 재시도가 필요하다면 retry.backoff.ms 시간만큼 재시도 전에 대기하고 재시도합니다. deliver.timeout.ms는 producer 메시지 전송에 허용된 최대 시간인데 이보다 더 오래 걸리면 재시도를 하지 않고 timeout exception을 보낸다. 따라서 &lt;b&gt;delivery.timeout.ms &amp;gt;= linger.ms + request.timeout.ms&lt;/b&gt;가 성립해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중복 없이 전송(idempotence)&lt;/h2&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;1102&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SRVpS/btsEFQqyjty/jdApDUaEagfR6rb8kEvTHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SRVpS/btsEFQqyjty/jdApDUaEagfR6rb8kEvTHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SRVpS/btsEFQqyjty/jdApDUaEagfR6rb8kEvTHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSRVpS%2FbtsEFQqyjty%2FjdApDUaEagfR6rb8kEvTHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1102&quot; height=&quot;930&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;930&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;프로듀서에서 데이터를 전송하고 브로커에서 데이터를 잘 받았으나 ack 응답이 네트워크 장애 등으로 인해 유실되었을 경우, 프로듀서는 브로커부터 ack 응답을 받지 못했기 때문에 재시도를 수행할 수 있습니다. 이 경우에 브로커에 중복된 메시가 전송되는 이슈가 발생하게 됩니다. 이를 해결하기 위해 프로듀서는 브로커로 메시지를 전송할 때 producerId(PID)와 sequence를 Header에 저장하여 전송합니다. 메시지 Sequence는 메시지 고유한 번호이고 0부터 시작하여 순차적으로 증가하나 producer가 기동시마다 새롭게 생성됩니다. 이를 활용하여 브로커에서는 메시지 sequence가 중복되면 메시지를 기록하지 않고 ack만 응답하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;idempotence를 위한 프로듀서에는 enable.idempotence=true, acks=all, retries는 0보다 큰 값, max.in.flight.requests.per.connection는 1에서 5사이 값으로 해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트랜잭션 프로듀서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 프로듀서는 다수의 파티션에 데이터를 저장할 경우 모든 데이터에 대해 동일한 원자성(atomic)을 만족시키기 위해 사용됩니다. 원자성을 만족시킨다는 의미는 다수의 데이터를 동일 트랜잭션으로 묶음으로써 전체 데이터를 처리하거나 전체 데이터를 처리하지 않도록 하는 것을 의미합니다. 컨슈머는 기본적으로 파티션에 쌓이는 대로 모두 가져가서 처리합니다. 하지만 트랜잭션으로 묶인 데이터를 브로커에서 가져갈 때는 다르게 동작하도록 설정할 수 있습니다. 트랜잭션 프로듀서를 사용하려면 enable.idempotence를 true로 설정하고 transactional.id를 임의의 String 값으로 정의합니다. 그리고 컨슈머의 isolation.level을 read_committed로 설정하면 프로듀서와 컨슈머는 트랜잭션으로 처리 완료된 데이터만 쓰고 읽게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 프로듀서는 사용자가 보낸 데이터를 레코드로 파티션에 저장할 뿐만 아니라 트랜잭션의 시작과 끝을 표현하기 위해 트랜잭션 레코드를 한 개 더 보냅니다. 트랜잭션 컨슈머는 파티션에 저장된 트랜잭션 레코드를 보고 트랜잭션이 완료(commit)되었음을 확인하고 데이터를 가져갑니다. 만약 데이터만 존재하고 트랜잭션 레코드가 존재하지 않으면 아직 트랜잭션이 완료되지 않았다고 판단하고 컨슈머는 데이터를 가져가지 않습니다. 트랜잭션 레코드는 실질적인 데이터는 가지고 있지 않고 트랜잭션이 끝난 상태를 표기하는 정보만 가지고 있습니다. 대신 레코드의 특성은 그대로 가지고 있어 오프셋 하나를 차지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 튜닝 파라미터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;메모리 사용량
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;buffer.memory
&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Record accumulator의 전체 메모리 사이즈&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;32 MB&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;record batch size, 압축&lt;/span&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;batch.size&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;단일 배치의 사이즈&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 16KB&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;compression.type&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 none, gzip, snappy, lz4, zdt 옵션이 존재&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;압축은 데이터를 배치에 모아 일괄로 처리하므로 얼마나 효율적으로 배치를 구성했는지에 따라 압축률이 달라진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;request size&lt;/span&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;max.request.size&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;요청의 최대 크기(바이트)프로듀서가 단일 요청으로 전송할 레코드 배치 수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 1MB&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;acks&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;지정시간에 request 에 대한 완료(ack) 회신 방식으로 성능이 떨어져도 보통 all을 권장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;acks = 0 : 응답 없이 동작(가장 빠름, 유실확률 높음)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;acks = 1 : Leader write 완료 시 회신, follower의 복제 여부는 확인하지 않음(중간 속도, 중간 유실 정도)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;acks = all(-1) : 최소 ISR(in-sync-replicas)수까지 복제 완료시 회신(가장 느림, 유실 확률 낮음)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;request connection
&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;linger.ms&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;sender thread로 메시지를 보내기 전 배치로 메시지를 만들어서 보내기 위한 최대 대기시간&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 0ms&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;max.in.flight.requests.per.connection&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;sender thread가 파티션별로 한번에 전송할 수 있는 배치의 개수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 5&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;값을 넘어가면 블로킹된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;1보다 큰 설정 시, 전송 실패에 재시도로 인해 메시지 순서가 변경될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본값에 의해 5개가 넘어갔는데 중간에 몇개가 실패하면 그것들이 재시도되면서 순서가 바뀐다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;send.buffer.bytes&lt;/span&gt;&lt;br /&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;카프카 프로듀서가 브로커 서버와 통신할 때 사용하는 소켓의 TCP 버퍼 크기를 의미한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본 128KB&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;-1&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;설정&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;시&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;운영체제의&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기본&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;설정값을&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kafka</category>
      <category>Kafka</category>
      <category>producer</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/76</guid>
      <comments>https://backtony.tistory.com/76#entry76comment</comments>
      <pubDate>Fri, 9 Feb 2024 18:05:35 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 구성요소</title>
      <link>https://backtony.tistory.com/75</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;카프카 브로커&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3442&quot; data-origin-height=&quot;1548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bczSNN/btsEEDMy8MH/k2Vd3D4514i514rQv3UyWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bczSNN/btsEEDMy8MH/k2Vd3D4514i514rQv3UyWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bczSNN/btsEEDMy8MH/k2Vd3D4514i514rQv3UyWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbczSNN%2FbtsEEDMy8MH%2Fk2Vd3D4514i514rQv3UyWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3442&quot; height=&quot;1548&quot; data-origin-width=&quot;3442&quot; data-origin-height=&quot;1548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 브로커는 카프카 클라이언트와 데이터를 주고받기 위해 사용하는 주체이자, 데이터를 분산 저장하여 장애가 발생하더라도 안전하게 사용할 수 있도록 도와주는 애플리케이션입니다. 하나의 서버에는 한 개의 카프카 브로커 프로세스가 실행되며, 보통은 데이터를 안전하게 보관하고 처리하기 위해 3대 이상의 브로커 서버를 1개의 클러스터로 묶어서 운영합니다. 카프카 클러스터로 묶인 브로커들은 카프카 프로듀서가 보낸 데이터를 안전하게 분산 저장하고 복제하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서로부터 데이터를 전달받으면 카프카 브로커는 프로듀서가 요청한 토픽의 파티션에 데이터를 저장하고 컨슈머가 데이터를 요청하면 파티션에 저장된 데이터를 전달합니다. 프로듀서로부터 전달된 데이터는 메모리, 데이터베이스, 캐시 메모리에 저장되지 않으며, 파일 시스템에 저장됩니다. 파일 시스템에 저장하기 때문에 속도 이슈가 있다고 생각할 수 있지만 카프카는 페이지 캐시를 사용하여 디스크 입출력 속도를 높여 속도 이슈를 해결했습니다. 이러한 특징 때문에 카프카 브로커를 실행하는데 힙 메모리 사이즈를 크게 설정할 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 캐시란 OS에서 파일 입출력의 성능 향상을 위해 만들어 놓은 메모리 영역을 말합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨트롤러&lt;/b&gt;&lt;br /&gt;클러스터의 다수 브로커 중 한 대가 컨트롤러의 역할을 합니다. 컨트롤러는 다른 브로커들의 상태를 체크하고 브로커가 클러스터에서 빠지는 경우 해당 브로커에 존재하는 리더 파티션을 재분배합니다. 만약 컨트롤러 역할을 하는 브로커에 문제가 생기면 다른 브로커가 컨트롤러 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코디네이터&lt;/b&gt;&lt;br /&gt;클러스터의 다수 브로커 중 한 대는 코디네이터 역할을 수행합니다. 코디네이터는 컨슈머 그룹의 상태를 체크하고 파티션을 컨슈머와 매칭되도록 분배하는 역할을 합니다. 컨슈머가 컨슈머 그룹에서 빠지면 매칭되지 않은 파티션을 정상 동작하는 컨슈머로 할당하여 끊임없이 데이터가 처리되도록 도와줍니다. 이렇게 파티션을 컨슈머로 재할당하는 과정을 &lt;b&gt;리밸런스&lt;/b&gt; 라고 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주키퍼(deprecated)&lt;br /&gt;카프카 4.0 버전 이전에는 파티션의 위치, 토픽의 설정 정보 같은 메타데이터를 외부 시스템인 주키퍼를 통해 관리했습니다. 하지만 주키퍼의 메타데이터와 브로커의 메타데이터의 불일치가 발생하면서 4.0버전부터는 외부 시스템인 주키퍼가 제거되고 카프카 내부적으로 관리되기 시작했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;토픽과 파티션&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R8Do6/btsEELKkZAY/O3uSWHqP61zE2ZkCCljbvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R8Do6/btsEELKkZAY/O3uSWHqP61zE2ZkCCljbvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R8Do6/btsEELKkZAY/O3uSWHqP61zE2ZkCCljbvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR8Do6%2FbtsEELKkZAY%2FO3uSWHqP61zE2ZkCCljbvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;528&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 프로듀서에서 데이터 발송 -&amp;gt; 카프카 브로커에 저장 -&amp;gt; 카프카 컨슈머가 브로커로부터 데이터를 받아서 처리&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 데이터 흐름은 위와 같이 구성됩니다. 카프카 브로커는 데이터를 구분하기 위해 사용하는 단위로 토픽은 1개 이상의 파티션을 보유하고 있습니다. 파티션에는 프로듀서가 전송한 데이터들이 저장되는데 이 데이터를 &lt;b&gt;레코드&lt;/b&gt;라고 합니다. 카프카 프로듀서는 브로커로 데이터를 전송할 때, 토픽을 반드시 지정해야 하며, 보통 파티션은 지정하지 않으나 특수한 경우라면 파티션도 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽 안에 여러 개의 파티션이 있고 파티션 안에 저장된 데이터들 레코드라고 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;2096&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKrX4N/btsEE0Ascc3/8vd3fa53zyVTXLaUnkQpJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKrX4N/btsEE0Ascc3/8vd3fa53zyVTXLaUnkQpJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKrX4N/btsEE0Ascc3/8vd3fa53zyVTXLaUnkQpJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKrX4N%2FbtsEE0Ascc3%2F8vd3fa53zyVTXLaUnkQpJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2096&quot; height=&quot;1166&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;1166&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;카프카는 여러 컨슈머가 묶인 컨슈머 그룹을 제공하고, 각 컨슈머 그룹이 토픽을 지정하여 카프카 브로커로부터 데이터를 소비합니다. 파티션은 카프카의 병렬처리의 핵심으로써 컨슈머 그룹이 토픽을 지정하게 되면 파티션 단위로 컨슈머와 매핑되게 됩니다. 파티션과 컨슈머는 N:1 매핑관계로 컨슈머는 여러 파티션을 처리할 수 있지만 하나의 파티션이 여러 컨슈머에 매핑될 수는 없습니다. 하지만 카프카 컨슈머 그룹은 그룹간의 격리된 환경을 제공하기 때문에 컨슈머 그룹이 다르다면 같은 토픽을 구독해도 서로에게 영향을 주지 않습니다. 그러므로 파티션과 컨슈머는 N:1 관계이지만 위 그림과 같이 다른 컨슈머 그룹인 경우에는 문제가 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 토픽 생성 시점에 파티션의 개수를 지정할 수 있는데 이후 파티션의 개수를 늘릴 수는 있지만 줄일 수는 없으므로 다음과 같은 내용을 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서와 컨슈머 데이터 처리량&lt;/li&gt;
&lt;li&gt;메시지 키 사용 여부&lt;/li&gt;
&lt;li&gt;브로커 영향도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 처리량&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 컨슈머 서버의 사양을 스케일 업해서 처리량을 높이는 데에는 한계가 있기 때문에 파티션의 개수를 늘리고 파티션 개수만큼 컨슈머를 늘리는 방법이 성능 향상에 가장 확실한 방법입니다. 그러므로 프로듀서가 보내는 데이터양과 컨슈머의 데이터 처리량을 계산해서 파티션 개수를 정하면 됩니다. 만약 프로듀서가 보내는 데이터가 초당 1,000 레코드이고 컨슈머가 처리할 수 있는 데이터가 초당 100레코드라면 최소한 필요한 파티션 개수는 10개라고 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로듀서 전송 데이터량 &amp;lt; 컨슈머 데이터 처리량 * 파티션 개수&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 개수만큼 컨슈머 스레드를 운영한다면 토픽의 병렬 처리를 극대화할 수 있습니다. 반면에 전체 컨슈머 데이터 처리량이 프로듀서가 보내는 데이터보다 적다면 컨슈머 랙이 생기고, 데이터 처리 지연이 발생하게 됩니다. 따라서, 컨슈머 전체 데이터 처리량이 프로듀서 데이터 처리량보다 많아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 키&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQbOmW/btsEIQC9XMp/St0xNKHUQtxzWi3ebNsGX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQbOmW/btsEIQC9XMp/St0xNKHUQtxzWi3ebNsGX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQbOmW/btsEIQC9XMp/St0xNKHUQtxzWi3ebNsGX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQbOmW%2FbtsEIQC9XMp%2FSt0xNKHUQtxzWi3ebNsGX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1624&quot; height=&quot;774&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 키의 사용 여부는 데이터 처리 순서와 관련이 있습니다. 카프카 프로듀서에서 브로커로 메시지를 보낼 때, 메시지 키를 지정할 수 있습니다. 메시지 키를 지정하지 않으면 스티키 파티셔닝(2.4버전 이후)방식으로 특정 파티션으로 전송되는 하나의 배치에 메시지를 빠르게 먼저 채워서 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 메시지 키를 사용하면 메시지 키를 사용하면 프로듀서가 토픽으로 데이터를 보낼 때 메시지 키를 해시 변환하여 메시지를 파티션에 매칭시킵니다. 따라서 메시지 키가 같다면 항상 같은 파티션으로 메시지가 전송되므로 순서를 보장할 수 있습니다. 하지만 파티션 개수가 달라지면 매칭이 깨지고 다른 파티션에 데이터가 할당되기 때문에 이때부터 컨슈머는 특정 메시지 키의 순서를 보장받지 못합니다. 즉, 메시지 키를 사용하고 처리 순서가 보장되어야 한다면 최대한 파티션의 변화가 발생하지 않는 방식으로 운영해야 합니다. 만약 파티션 개수가 변해야 하는 경우에는 기존에 사용하던 메시지 키의 매칭을 그대로 가져가기 위해 커스텀 파티셔너를 개발하고 적용해야 합니다. 이러한 어려움 때문에 보통 메시지 키별로 처리 순서를 보장하기 위해서는 파티션 개수를 프로듀서가 전송하는 데이터양보다 더 넉넉하게 잡고 생성하는 것이 권장됩니다. 반면에 처리 순서가 관계없다면 처음부터 넉넉하게 잡지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브로커 영향도&lt;/b&gt;&lt;br /&gt;카프카에서 파티션은 각 브로커의 파일 시스템을 사용하기 때문에 파티션이 늘어나는 만큼 브로커에서 접근하는 파일 개수가 많아집니다. 운영체제에서는 프로세스당 열 수 있는 파일 최대 개수를 제한하고 있기 때문에 안정적으로 유지하기 위해서는 각 브로커당 파티션 개수를 모니터링 해야합니다. 데이터양이 많아져서 파티션 개수를 늘려야 하는 상황이라면 브로커당 파티션 개수를 확인하고 진행해야 합니다. 만약 브로커가 관리하는 파티션 개수가 너무 많다면 파티션 개수를 분산하기 위해 카프카 브로커 개수를 늘리는 방안도 같이 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레코드와 오프셋&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lr2dN/btsEGMOMUD6/jAflTDO0OBCtPN9zWGueT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lr2dN/btsEGMOMUD6/jAflTDO0OBCtPN9zWGueT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lr2dN/btsEGMOMUD6/jAflTDO0OBCtPN9zWGueT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flr2dN%2FbtsEGMOMUD6%2FjAflTDO0OBCtPN9zWGueT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1552&quot; height=&quot;918&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;레코드는 타임스탬프, 메시지 키, 메시지 값, 오프셋으로 구성됩니다. 프로듀서가 생성한 레코드가 브로커로 전송되면 오프셋과 타임스탬프가 지정되어 저장됩니다.(필요에 따라 프로듀서에서 레코드 생성 시간, 다른 시간으로 타임스탬프 지정 가능) 브로커에 한번 적재된 레코드는 수정할 수 없고 로그 리텐션 기간 또는 용량에 따라서만 삭제됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드는 저장될 때, 오프셋 값이 부여됩니다. 오프셋은 컨슈머 그룹이 데이터를 어디까지 읽어갔는지 확인하는 용도로 사용됩니다. 레코드의 오프셋은 직접 지정할 수 없고 브로커에 저장될 때 이전에 전송된 레코드의 오프셋 + 1 의 값으로 생성됩니다. 컨슈머 그룹은 토픽의 특정 파티션으로부터 데이터를 가져가서 처리하고 파티션의 어느 레코드까지 읽었는지 알리기 위해 오프셋을 커밋합니다. 커밋한 오프셋은 consumer_offsets 토픽에 저장되고 저장된 오프셋을 토대로 컨슈머 그룹은 다음 레코드를 가져가서 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 삭제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 다른 메시징 플랫폼과 다르게 컨슈머가 데이터를 가져가더라도 토픽의 데이터는 삭제되지 않습니다. 또한 컨슈머나 프로듀서가 데이터 삭제 요청을 할 수 없고, 오직 브로커만이 데이터를 삭제할 수 있습니다. 데이터 삭제는 파일 단위로 이뤄지는데 이 단위를 &lt;b&gt;로그 세그먼트&lt;/b&gt;라고 합니다. 이 세그먼트에는 다수의 데이터가 들어 있기 때문에 일반적인 데이터베이스처럼 특정 데이터를 선별해서 삭제할 수 없습니다. 세그먼트는 데이터가 쌓이는 동안 파일 시스템으로 열려있으며 카프카 브로커에 log.segment.bytes 또는 log.segment.ms 값이 설정되면 설정값에 따라 세그먼트 파일이 닫힙니다. 세그먼트 파일이 닫히게 되는 기본값은 1GB입니다. 닫힌 세그먼트 파일은 log.retention.bytes 또는 log.retention.ms 값이 넘으면 삭제 됩니다. 닫힌 세그먼트 파일을 체크하는 간격은 카프카 브로커의 옵션에 설정된 log.retention.check.interval.ms에 따릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 복제, 싱크&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEsV1Z/btsEFd0DghM/4RNKKhuzbw9xNwMs1zhgbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEsV1Z/btsEFd0DghM/4RNKKhuzbw9xNwMs1zhgbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEsV1Z/btsEFd0DghM/4RNKKhuzbw9xNwMs1zhgbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEsV1Z%2FbtsEFd0DghM%2F4RNKKhuzbw9xNwMs1zhgbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;672&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;카프카는 데이터 복제를 통해 클러스터로 묶인 브로커 중 일부에 장애가 발생하더라도 데이터를 유실하지 않고 안전하게 동작하기 위해 파티션 단위로 복제가 이뤄집니다. 토픽을 생성할 때 파티션의 복제 개수도 같이 설정하는데 직접 옵션을 선택하지 않으면 브로커에 설정된 옵션 값을 따라갑니다. 복제 개수의 최솟값은 1(복제없음)이고 값최댓값은 브로커 개수만큼 사용할 수 있습니다. 만약 복제 개수가 3(자신+복제2개)으로 총 3개의 파티션이 구성된다면 리더 파티션과 팔로워 파티션으로 구성됩니다. 프로듀서 또는 컨슈머와 직접 통신하는 파티션을 리더 파티션, 복제 데이터를 갖는 나머지 파티션을 팔로워 파티션이라고 합니다. 팔로워 파티션들은 리더 파티션의 오프셋을 확인하여 현재 자신이 가지고 있는 오프셋과 차이가 나는 경우 리더 파티션으로부터 데이터를 가져와서 자신의 파티션에 복제합니다. 만약 리더 파티션을 갖고 있는 브로커에 장애가 발생해 다운되면 팔로워 파티션 중 하나가 리더 파티션 지위를 넘겨받습니다. 이를 통해 데이터가 유실되지 않고 컨슈머, 프로듀서가 데이터를 주고받도록 동작할 수 있게 됩니다.&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Kafka</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/75</guid>
      <comments>https://backtony.tistory.com/75#entry75comment</comments>
      <pubDate>Fri, 9 Feb 2024 17:59:15 +0900</pubDate>
    </item>
    <item>
      <title>Spring - OpenFeign</title>
      <link>https://backtony.tistory.com/74</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;본 포스팅은 spring boot 3.2.2 버전을 기준으로 작성되었습니다.&lt;br /&gt;공부한 내용을 정리하는&amp;nbsp;&lt;a href=&quot;https://backtony.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt;와 관련 코드를 공유하는&amp;nbsp;&lt;a href=&quot;https://github.com/backtony/blog/tree/main/category/spring/feign/open-feign&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;이 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;open feign이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feign은 Netflix에서 개발된 Http client binder로 REST Call을 위해 호출하는 클라이언트를 보다 쉽게 작성할 수 있도록 도와주는 라이브러리입니다. spring의 경우 &lt;b&gt;spring-cloud-starter-openfeign&lt;/b&gt; 라이브러리 추가로 사용할 수 있습니다. spring cloud는 spring mvc annotation에 대한 지원과 sprinb web에서 사용되는 것과 동일한 HttpMessageConverters를 지원합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존성 및 client&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    implementation(&quot;org.springframework.cloud:spring-cloud-starter-openfeign&quot;)
    // okhttp
    implementation(&quot;io.github.openfeign:feign-okhttp&quot;)
    // apache
//    implementation(&quot;io.github.openfeign:feign-hc5&quot;)
    implementation(&quot;io.github.openfeign:feign-jackson&quot;)
}

extra[&quot;springCloudVersion&quot;] = &quot;2023.0.0&quot;

dependencyManagement {
    imports {
        mavenBom(&quot;org.springframework.cloud:spring-cloud-dependencies:${property(&quot;springCloudVersion&quot;)}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign은 Apache HttpClient, OkHttp Client 등 다양한 HTTP 클라이언트를 주입받아서 동작합니다. 특별한 설정을 하지 않으면 Feign이 제공하는 기본 클라이언트를 사용합니다. java 17 버전 이전의 경우, 기본 클라이언트는 HttpURLConnection 클래스를 사용하기 때문에 동시성 문제가 발생할 수 있습니다. 따라서 공식 문서에 언급되어 있는 apache 또는 okhttp 사용이 권장됩니다.&lt;/p&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;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  cloud:
    openfeign:
      # okhttp의 경우
      okhttp:
        enabled: true
      # apache의 경우    
      httpclient:
        hc5:
          enabled: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 client에 따라 application.yml에 설정을 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@EnableFeignClients&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@EnableFeignClients
@SpringBootApplication
class ClientApplication

fun main(args: Array&amp;lt;String&amp;gt;) {
    runApplication&amp;lt;ClientApplication&amp;gt;(*args)
}&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;@EnableFeignClients는 @FeignCleint 애노테이션이 붙은 클래스를 찾아다니면서 구현체를 만들어줍니다. 따라서 root package에 있어야 하며, 그렇지 않은 경우 basePackages 또는 basePackageClasses를 지정해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@FeignClient&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;article&quot;)
interface ArticleClient {

    @PostMapping(&quot;/articles&quot;)
    fun save(@RequestBody articleSaveCommand: ArticleSaveCommand): ArticleResponse

    @GetMapping(&quot;/articles/{id}&quot;)
    fun get(@PathVariable id: String): ArticleResponse?

    @PatchMapping(&quot;/articles/{id}&quot;)
    fun update(@PathVariable id: String, @RequestBody articleUpdateCommand: ArticleUpdateCommand): ArticleResponse?

    @DeleteMapping(&quot;/articles/{id}&quot;)
    fun delete(@PathVariable id: String): ArticleResponse?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스만 위와 같이 만들어주고 사용하는 곳에서는 아래와 같이 가져다 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class ArticleTest(
    private val articleClient: ArticleClient,
) {

    fun test() {
        articleClient.save(command)
        articleClient.update(id, command)
        articleClient.delete(id)
        articleClient.get(id)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;timeout&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클라이언트에 대한 url 및 timeout 설정은 application.yml에서 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  cloud:
    openfeign:
      okhttp:
        enabled: true
      client:
        config:
          article: # feign client name
            url: http://localhost:8081
            connectTimeout: 3000
            readTimeout: 3000
          default:
            connectTimeout: 3000
            readTimeout: 3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;spring.cloud.openfeign.client.config.XXX&lt;/b&gt;에 클라이언트에 대한 설정을 명시할 수 있습니다. 앞서 &lt;b&gt;@FeignClient(name = &quot;article&quot;)&lt;/b&gt;에 name이 article이므로 article 클라이언트에 대한 설정은 위와 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707031392226&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;spring.cloud.openfeign.httpclient&quot;)
public class FeignHttpClientProperties {}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class,
		FeignEncoderProperties.class })
public class FeignAutoConfiguration {
}&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;이외의 추가적인 client에 대한 properties는 &lt;b&gt;org.springframework.cloud.openfeign.support.FeignHttpClientProperties&lt;/b&gt;에서 확인할 수 있습니다. 그리고 해당 properties를 사용하여 client를 만드는 과정은 &lt;b&gt;org.springframework.cloud.openfeign.FeignAutoConfiguration&lt;/b&gt; 클래스에서 확인할 수 있습니다. 특정 client에 대한 설정이 아닌 global 설정은 default에서 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;## 로깅 DEBUG 설정
logging.level.&amp;lt;packageName&amp;gt;.&amp;lt;className&amp;gt; = DEBUG
logging.level.&amp;lt;packageName&amp;gt; = DEBUG


# ex
logging.level.com.example.client.client.ArticleClient: DEBUG&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;feign에 대한 로깅을 활성화하려면 application.yml에 feign 클라이언트가 포함된 클래스나 패키지에 대해서 로깅 수준을 DEBUG로 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:
    cloud:
    openfeign:
        client:
            config:
            article:
                # 로깅 레벨 설정  
                loggerLevel: basic&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;그리고 각 feign 클라이언트에 대한 로깅 수준을 설정할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NONE : 로깅하지 않음(default)&lt;/li&gt;
&lt;li&gt;BASIC : 요청 method, url, 응답 상태 코드, 실행 시간&lt;/li&gt;
&lt;li&gt;HEADERS : basic + request, response header&lt;/li&gt;
&lt;li&gt;FULL : headers + body + meta data for both requests, response&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@FeignClient Configuration&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;article&quot;, configuration = [ArticleFeignConfig::class])
interface ArticleClient&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;@FeignClient의 configuration 속성으로 client에 적용될 default config를 override 할 수 있습니다. 공식문서에 따르면 아래와 같은 config가 default로 세팅됩니다.&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;1966&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8axrc/btsEopl9l4B/9pZUoXTFo6d46rPgu5IGZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8axrc/btsEopl9l4B/9pZUoXTFo6d46rPgu5IGZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8axrc/btsEopl9l4B/9pZUoXTFo6d46rPgu5IGZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8axrc%2FbtsEopl9l4B%2F9pZUoXTFo6d46rPgu5IGZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1966&quot; height=&quot;1226&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Interceptor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;article&quot;, configuration = [ArticleFeignConfig::class])
interface ArticleClient

@Configuration
class ArticleFeignConfig {

    @Bean
    fun authorizationHeaderInterceptor() = RequestInterceptor {
        it.header(HttpHeaders.AUTHORIZATION, &quot;Bearer ${UUID.randomUUID()}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interceptor를 사용해서 헤더를 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;error handling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;article&quot;, configuration = [ArticleFeignConfig::class])
interface ArticleClient

@Configuration
class ArticleFeignConfig {

    @Bean
    fun errorDecoder(): ErrorDecoder {
        return ErrorDecoder { _, response -&amp;gt;
            when (response.status()) {
                401 -&amp;gt; RuntimeException(&quot;401 발생&quot;)
                500 -&amp;gt; RuntimeException(&quot;500 발생&quot;)
                else -&amp;gt; RuntimeException(&quot;전대미문 article error 발생!&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 feign 클라이언트에서 예외가 발생하면 FeignException이 발생합니다. ErrorDecoder를 구현해서 빈으로 등록하면 Error처리를 별도로 핸들링할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 애노테이션을 붙이면 전역적으로 등록되고, @Configuration 애노테이션 없이 @FeignClient의 속성으로 등록하면 해당 client에만 적용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;retry&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;article&quot;, configuration = [ArticleFeignConfig::class])
interface ArticleClient

@Configuration
class ArticleFeignConfig {

    @Bean
    fun retryer(): Retryer {
        // 1초를 시작으로 1.5를 곱하면서 재시도
        // 재시도 최대 간격은 2초
        // 최대 3번까지만 재시도
        return Retryer.Default(1000, 2000, 3)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retryer의 경우 별도로 등록하지 않으면 Retryer.NEVER_RETRY 타입의 retryer 빈이 자동으로 등록되어 재시도를 비활성화합니다. 하지만 retryer 빈을 위와 같이 별도로 등록하 경우, IOException이 발생하거나 errorDecoder에서 retryableException이 발생하게 되면 재시도를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;article&quot;, configuration = [ArticleFeignConfig::class])
interface ArticleClient

@Configuration
class ArticleFeignConfig {

    @Bean
    fun retryer(): Retryer {
        return Retryer.Default(1000, 2000, 3)
    }

    @Bean
    fun errorDecoder(): ErrorDecoder {
        return ErrorDecoder { _, response -&amp;gt;
            when (response.status()) {
                401 -&amp;gt; RuntimeException(&quot;401 발생&quot;)
                500 -&amp;gt;
                    RetryableException(
                        response.status(),
                        &quot;500 에러 발생, 재시도합니다.&quot;,
                        response.request().httpMethod(),
                        1, // retryer에서 설정한 최대 시간보다는 작아야함.
                        response.request(),
                    )

                else -&amp;gt; RuntimeException(&quot;전대미문 article error 발생!&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;file up / download&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직까지 feign에서는 request에 대한 stream upload를 지원하진 않고, stream download만 지원합니다.&lt;/p&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;feign file stream 관련 이슈&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/OpenFeign/feign/issues/220&quot;&gt;https://github.com/OpenFeign/feign/issues/220&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/OpenFeign/feign/issues/1243&quot;&gt;https://github.com/OpenFeign/feign/issues/1243&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 feign client에는 별도의 설정이 없다면 FeignClientsConfiguration클래스의 feignEncoder 메서드로 인해 SpringEncoder가 빈으로 등록됩니다. 그리고 해당 코드를 따라 들어가다 보면 SpringEncoder가 등록되는데 아래와 같은 코드로 등록됩니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnMissingClass(&quot;org.springframework.data.domain.Pageable&quot;)
    public Encoder feignEncoder(ObjectProvider&amp;lt;AbstractFormWriter&amp;gt; formWriterProvider,
    ObjectProvider&amp;lt;HttpMessageConverterCustomizer&amp;gt; customizers) {
        return springEncoder(formWriterProvider, encoderProperties, customizers);
    }

    private Encoder springEncoder(ObjectProvider&amp;lt;AbstractFormWriter&amp;gt; formWriterProvider,
    FeignEncoderProperties encoderProperties, ObjectProvider&amp;lt;HttpMessageConverterCustomizer&amp;gt; customizers) {
        AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();

        if (formWriter != null) {
            return new SpringEncoder(new SpringPojoFormEncoder(formWriter), messageConverters, encoderProperties,
            customizers);
        }
        else {
            return new SpringEncoder(new SpringFormEncoder(), messageConverters, encoderProperties, customizers);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-cloud-starter-openfeign 의존성에 의해 spring web에서 사용하는 messageConverters가 주입되기 때문에 별도의 세팅 없이 multipart/form-data 형식의 데이터를 처리할 수 있습니다. 따라서 업로드의 경우에는 multipart/form-data를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;article&quot;, configuration = [ArticleFeignConfig::class])
interface ArticleClient {

    @PostMapping(value = [&quot;/upload&quot;], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
    fun upload(@RequestPart file: MultipartFile): String

    @GetMapping(&quot;/download/{path}&quot;)
    fun download(@PathVariable path: String): Response
}&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;download의 경우에는 feign package의 Response 클래스를 사용하여 아래와 같이 스트림으로 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RestController
class ArticleController(
    private val articleClient: ArticleClient,
) {

    @GetMapping(&quot;/download/{path}&quot;)
    fun download(@PathVariable path: String, response: HttpServletResponse) {

        articleClient.download(path).body().asInputStream().use { ins -&amp;gt;
            response.outputStream.use { os -&amp;gt; ins.transferTo(os) }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign&quot;&gt;공식문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>Cloud</category>
      <category>OpenFeign</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/74</guid>
      <comments>https://backtony.tistory.com/74#entry74comment</comments>
      <pubDate>Sun, 4 Feb 2024 16:18:14 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - 병럴 처리</title>
      <link>https://backtony.tistory.com/72</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;단일 스레드 vs 멀티 스레드&lt;/h2&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;804&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tqYO3/btsEkAP5g8V/f7KfJf68OUIcT5HOqx3WIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tqYO3/btsEkAP5g8V/f7KfJf68OUIcT5HOqx3WIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tqYO3/btsEkAP5g8V/f7KfJf68OUIcT5HOqx3WIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtqYO3%2FbtsEkAP5g8V%2Ff7KfJf68OUIcT5HOqx3WIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;804&quot; height=&quot;390&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스 내 특정 작업을 처리하는 스레드가 하나일 경우 단일 스레드, 여러 개일 경우 멀티 스레드라고 합니다. 작업 처리에 있어서 단일 스레드와 멀티 스레드의 선택 기준은 어떤 방식이 자원을 효율적으로 사용하고 성능 처리에 유리한가 하는 점입니다. 일반적으로 복잡한 처리나 대용량 데이터를 다루는 작업일 경우 전체 소요 시간 및 성능상의 이점을 가져오기 위해 멀티 스레드 방식을 사용합니다. 하지만 멀티 스레드 처리 방식은 데이터 동기화 이슈가 존재하기 때문에 주의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 배치 스레드 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치에서 멀티스레드 환경을 구성하기 위해서 가장 먼저 해야 할 일은 사용하고자 하는 Reader와 Writer가 멀티스레드를 지원하는지 확인하는 것입니다. 스프링 배치 모델에서 지원하는 구현체의 reader와 writer를 타고 들어가 보면 주석으로 thread-safe 한지 여부를 확인할 수 있습니다. 그리고 &lt;b&gt;멀티스레드로 각 chunk들이 개별로 진행되다보니 spring batch의 큰 장점 중 하나인 실패 지점에서 재시작하는 것이 불가능합니다.&lt;/b&gt; 단일 스레드로 순차적으로 실행할때는 10번째 청크가 실패한다면 9번째 청크까지는 성공했음이 보장되지만, 멀티스레드의 경우 1 ~ 10 개의 청크가 동시에 실행되다 보니 10번째 청크가 실패했다고 해서 1 ~ 9까지의 청크가 다 성공된 상태임이 보장되지 않습니다. 그래서 일반적으로 멀티 스레드 환경의 배치를 구성할 때는 ItemReader의 saveState 옵션을 false로 설정하고 사용합니다.(실패한 지점을 저장하지 못하게 해 다음 실행 시에도 무조건 처음부터 다시 읽도록 하는 옵션) 하지만 &lt;b&gt;파티셔닝 방식은 예외적으로 실패지점에서 재시작이 보장됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AsyncItemProcessor / AsyncItemWriter
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 쓰레드를 통해 ItemProcessor와 ItemWriter를 처리하는 방식&lt;/li&gt;
&lt;li&gt;spring-batch-integration 추가 의존성 필요&lt;/li&gt;
&lt;li&gt;보통 AsyncItemProcessor와 AsyncItemWriter를 함께 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Multi-thread Step
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 Step을 수행하는 경우, Step 내의 Chunk 단위마다 스레드가 할당되어 처리되는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Parallel Steps
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 Step을 수행하는 경우, Step마다 스레드가 할당되어 여러 개의 Step을 병렬로 실행하는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Remote Chunking
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산환경처럼 Step 처리가 여러 프로세스로 분할되어 외부의 다른 서버로 전송되어 처리하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Partitioning
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Master/Slave 구조&lt;/li&gt;
&lt;li&gt;매니저(Master)가 partitioner를 사용하여 각각의 slave에 넘겨줄 데이터 범위를 결정하고 Slave는 해당 데이터를 Chunk 단위로 독립적으로 처리하는 방식&lt;/li&gt;
&lt;li&gt;다른 멀티 스레드 모델과 달리 &lt;b&gt;실패 지점 재시작이 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AsyncItemProcessor / AsyncItemWriter&lt;/h3&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;944&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be5xTE/btsElItMK04/wDdabIqNf1bBQImGlzFqnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be5xTE/btsElItMK04/wDdabIqNf1bBQImGlzFqnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be5xTE/btsElItMK04/wDdabIqNf1bBQImGlzFqnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe5xTE%2FbtsElItMK04%2FwDdabIqNf1bBQImGlzFqnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;944&quot; height=&quot;315&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step 안에서 ItemProcessor가 비동기적으로 동작하는 구조입니다. AsyncItemProcessor / AsyncItemWriter 둘이 함께 구성되어야 합니다. AsyncItemProcessor로부터 AsyncItemWriter가 받는 최종 결괏값은 List&amp;lt;Future&amp;lt;T&amp;gt;&amp;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;1186&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3VUKk/btsElkMZYzS/foqVxIz78KKTsDd27hcARK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3VUKk/btsElkMZYzS/foqVxIz78KKTsDd27hcARK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3VUKk/btsElkMZYzS/foqVxIz78KKTsDd27hcARK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3VUKk%2FbtsElkMZYzS%2FfoqVxIz78KKTsDd27hcARK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1186&quot; height=&quot;606&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;606&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;AsyncItemProcessor는 ItemProcessor에 실제 작업을 위임합니다. TaskExecutor로 비동기 실행을 하기 위한 스레드를 만들고 해당 스레드는 FutureTask를 실행합니다. FutureTask는 Callable 인터페이스를 실행하면서 그 안에서 ItemProcessor가 작업을 처리하게 됩니다. 이런 하나의 단위를 AsyncItemProcessor가 제공해서 처리를 위임하고 메인 스레드는 바로 다음 AsyncItemWriter로 넘어갑니다. AsyncItemWriter도 ItemWriter에게 작업을 위임합니다. ItemWriter는 Future 안에 있는 item들을 꺼내서 일괄처리하게 되는데 이때 Processor에서 작업 중인 비동기 실행의 결괏값들을 모두 받아오기까지 대기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;br /&gt;사용하려면 Spring-batch-integration 의존성이 필요합니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.batch:spring-batch-integration'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Future&amp;lt;Customer2&amp;gt;&amp;gt;chunk(chunkSize) // Future 타입
                .reader(customItemReader())
                .processor(customAsyncItemProcessor())
                .writer(customAsyncItemWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;? extends Customer&amp;gt; customItemReader() {
        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;customItemReader&quot;)
                .pageSize(chunkSize)
                .entityManagerFactory(entityManagerFactory)
                .queryString(&quot;select c from Customer c order by c.id&quot;)
                .build();
    }

    @Bean
    public AsyncItemProcessor&amp;lt;Customer, Customer2&amp;gt; customAsyncItemProcessor() {
        AsyncItemProcessor&amp;lt;Customer, Customer2&amp;gt; asyncItemProcessor = new AsyncItemProcessor&amp;lt;&amp;gt;();
        asyncItemProcessor.setDelegate(customItemProcessor()); // customItemProcessor 로 작업 위임
        asyncItemProcessor.setTaskExecutor(new SimpleAsyncTaskExecutor()); // taskExecutor 세팅

        return asyncItemProcessor;
    }

    @Bean
    public ItemProcessor&amp;lt;Customer, Customer2&amp;gt; customItemProcessor() {
        return new ItemProcessor&amp;lt;Customer, Customer2&amp;gt;() {
            @Override
            public Customer2 process(Customer item) throws Exception {
                return new Customer2(item.getName().toUpperCase(), item.getAge());
            }
        };
    }


    @Bean
    public AsyncItemWriter&amp;lt;Customer2&amp;gt; customAsyncItemWriter() {
        AsyncItemWriter&amp;lt;Customer2&amp;gt; asyncItemWriter = new AsyncItemWriter&amp;lt;&amp;gt;();
        asyncItemWriter.setDelegate(customItemWriter()); // customItemWriter로 작업 위임
        return asyncItemWriter;
    }

    @Bean
    public ItemWriter&amp;lt;Customer2&amp;gt; customItemWriter() {
        return new JdbcBatchItemWriterBuilder&amp;lt;Customer2&amp;gt;()
                .dataSource(dataSource)
                .sql(&quot;insert into customer2 values (:id, :age, :name)&quot;)
                .beanMapped()
                .build();

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customer 데이터를 프로세서에서 Customer2객체로 전환하여 Writer로 전달하는 예시입니다. 사실상 코드는 동기 코드와 큰 차이 없이 위임하는 과정만 추가되었다고 봐도 무방합니다. 동기 Processor와 Writer을 만들고 비동기 Processor와 Writer를 만들어 그 안에서 위임하는 코드와 TaskExecutor 설정만 추가해 주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Multi-thread Step&lt;/h3&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;1105&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTXSUC/btsEnQEjyLw/cj5oSj50wuPcw7fLkRV5A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTXSUC/btsEnQEjyLw/cj5oSj50wuPcw7fLkRV5A0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTXSUC/btsEnQEjyLw/cj5oSj50wuPcw7fLkRV5A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTXSUC%2FbtsEnQEjyLw%2Fcj5oSj50wuPcw7fLkRV5A0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1105&quot; height=&quot;480&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;480&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step 내에서 멀티 스레드로 Chunk 기반 처리가 이뤄지는 구조 입니다.&lt;/li&gt;
&lt;li&gt;TaskExecutorRepeatTemplate이 반복자로 사용되며 설정한 개수(throttleLimit)만큼의 스레드를 생성하여 수행합니다.&lt;/li&gt;
&lt;li&gt;ItemReader는 반드시 Thread-safe인지 확인해야 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 소스로부터 읽어오는 역할이기 때문에 스레드마다 중복해서 데이터를 읽지 않도록 동기화가 보장되어야 합니다.&lt;/li&gt;
&lt;li&gt;스프링 배치에서 제공하는 &lt;b&gt;JdbcPagingItemReader, JpaPagingItemReader가 Thread-safe&lt;/b&gt; 하게 동작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스레드끼리는 Chunk를 공유하지 않고 스레드마다 새로운 Chunk가 할당되어 데이터 동기화가 보장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer2&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor())
                .writer(customItemWriter())
                .taskExecutor(taskExecutor())
                .build();
    }

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(4); // 기본 스레드 풀 크기
        taskExecutor.setMaxPoolSize(8); // 4개의 스레드가 이미 처리중인데 작업이 더 있을 경우 몇개까지 스레드를 늘릴 것인지
        taskExecutor.setThreadNamePrefix(&quot;async-thread&quot;); // 스레드 이름 prefix
        return taskExecutor;
    }

    @Bean
    public ItemReader&amp;lt;? extends Customer&amp;gt; customItemReader() {
        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;customItemReader&quot;)
                .pageSize(chunkSize)
                .entityManagerFactory(entityManagerFactory)
                .queryString(&quot;select c from Customer c order by c.id&quot;)
                .saveState(false)
                .build();
    }


    @Bean
    public ItemProcessor&amp;lt;Customer, Customer2&amp;gt; customItemProcessor() {
        return new ItemProcessor&amp;lt;Customer, Customer2&amp;gt;() {
            @Override
            public Customer2 process(Customer item) throws Exception {
                return new Customer2(item.getName().toUpperCase(), item.getAge());
            }
        };
    }


    @Bean
    public ItemWriter&amp;lt;Customer2&amp;gt; customItemWriter() {
        return new JdbcBatchItemWriterBuilder&amp;lt;Customer2&amp;gt;()
                .dataSource(dataSource)
                .sql(&quot;insert into customer2 values (:id, :age, :name)&quot;)
                .beanMapped()
                .build();

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 동기 코드에서 taskExecutor세팅만 추가해 주면 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parallel Steps&lt;/h3&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;1209&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc3yLo/btsEmXxbBtC/Wfuh5R3aoVosKWY0YC36Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc3yLo/btsEmXxbBtC/Wfuh5R3aoVosKWY0YC36Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc3yLo/btsEmXxbBtC/Wfuh5R3aoVosKWY0YC36Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc3yLo%2FbtsEmXxbBtC%2FWfuh5R3aoVosKWY0YC36Ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1209&quot; height=&quot;484&quot; data-origin-width=&quot;1209&quot; data-origin-height=&quot;484&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SplitState를 사용해서 여러 개의 Flow들을 병렬적으로 실행하는 구조입니다.&lt;/li&gt;
&lt;li&gt;실행이 다 완료된 후 FlowExecutionStatus 결과들을 취합해서 다음 단계를 결정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public Job parallelStepsJob() {

  Flow secondFlow = new FlowBuilder&amp;lt;Flow&amp;gt;(&quot;secondFlow&quot;)          // secondFlow 생성
                        .start(step2())                          // step2 실행 
                        .build();

  Flow parallelFlow = new FlowBuilder&amp;lt;Flow&amp;gt;(&quot;parallelFlow&quot;)        // parallelFlow 생성
                        .start(step1())                          // step1 실행
                        .split(new SimpleAsyncTaskExecutor())    // split 메서드에 TaskExecutor 를 파라미터로 주고 SplitBuilder 반환 
                        .add(secondFlow)                         // secondFlow 를 추가 -&amp;gt; 병렬로 실행 
                        .build();  

  return jobBuilderFactory.get(&quot;parallelStepsJob&quot;)
                        .start(parallelFlow)                     // parallelFlow 실행
                        .next(nextFlow())
                        .end()
                        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 잡에서는 스텝 내에서 처리해야 할 모든 아이템이 처리되기 전에는 해당 스텝이 완료되지 않으며 스텝이 완료되지 않았다면 다음 스텝이 시작되지 않습니다. 하지만 split 메서드를 사용했다면 split 메서드를 통해 add로 추가된 여러 step에 대해서 동시에 병렬로 수행되며, 구성된 모든 플로우가 완료될 때까지 이후 스텝은 실행되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SynchronizedItemStreamReader&lt;/h3&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;1033&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAOkKi/btsEopsU7ik/1X4yyZHhy1szN9aQkaWXUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAOkKi/btsEopsU7ik/1X4yyZHhy1szN9aQkaWXUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAOkKi/btsEopsU7ik/1X4yyZHhy1szN9aQkaWXUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAOkKi%2FbtsEopsU7ik%2F1X4yyZHhy1szN9aQkaWXUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1033&quot; height=&quot;502&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread-safe 하지 않은 ItemReader를 Thread-safe 하게 처리하도록 하는 기능을 제공합니다. 단순히 Thread-safe 하지 않은 ItemReader를 SynchronizedItemStreamReader로 한번 감싸주면 되기 때문에 적용 방식은 매우 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step())
                .incrementer(new RunIdIncrementer())
                .build();
    }



    @Bean
    public Step step() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer,Customer2&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .taskExecutor(taskExecutor())
                .build();
    }
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(4); // 기본 스레드 풀 크기
        taskExecutor.setMaxPoolSize(8); // 4개의 스레드가 이미 처리중인데 작업이 더 있을 경우 몇개까지 스레드를 늘릴 것인지
        taskExecutor.setThreadNamePrefix(&quot;async-thread&quot;); // 스레드 이름 prefix
        return taskExecutor;
    }

    @Bean
    public SynchronizedItemStreamReader&amp;lt;Customer&amp;gt; customItemReader() {
        // thread-safe 하지 않은 Reader
        JdbcCursorItemReader&amp;lt;Customer&amp;gt; notSafetyReader = new JdbcCursorItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;customItemReader&quot;)
                .dataSource(dataSource)
                .fetchSize(chunkSize)
                .rowMapper(new BeanPropertyRowMapper&amp;lt;&amp;gt;(Customer.class))
                .sql(&quot;select id, name, age from customer order by id&quot;)
                .build();

        // SyncStreamReader 만들고 인자로 thread-safe하지 않은 Reader를 넘기면 
        // Read하는 작업이 동기화 되서 진행된다.
        return new SynchronizedItemStreamReaderBuilder&amp;lt;Customer&amp;gt;()
                .delegate(notSafetyReader)
                .build();
    }

    @Bean
    public JdbcBatchItemWriter&amp;lt;Customer2&amp;gt; customItemWriter() {
        return new JdbcBatchItemWriterBuilder&amp;lt;Customer2&amp;gt;()
                .dataSource(dataSource)
                .sql(&quot;insert into customer2 values (:id, :age, :name)&quot;)
                .beanMapped()
                .build();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Partitioning&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝은 매니저 (마스터) Step이 대량의 데이터 처리를 위해 지정된 수의 작업자 (Slave) Step으로 일감을 분할 처리하는 방식을 말합니다.(Master/Slave 구조) 각 slave가 처리할 데이터의 범위는 partitioner의 StepExecution을 통해 지정할 수 있으며, Slave Step은 각 스레드에 의해 독립적으로 실행되어 병렬처리됩니다. 파티셔닝은 독립적인 step을 구성하며 각각 별도의 stepExecution을 가지고 처리하므로 reader/writer의 멀티스레드 지원 여부가 중요하지 않고, 다른 멀티 스레드 모델과 달리 &lt;b&gt;실패 지점 재시작이 가능&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구조&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9nzq0/btsEncN9nmZ/QjB5HiggADKs7dPaBevhd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9nzq0/btsEncN9nmZ/QjB5HiggADKs7dPaBevhd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9nzq0/btsEncN9nmZ/QjB5HiggADKs7dPaBevhd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9nzq0%2FbtsEncN9nmZ%2FQjB5HiggADKs7dPaBevhd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1214&quot; height=&quot;593&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;593&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;파티셔닝은 다른 멀티 스레드 방식과 달리 Partitioner와 PartitionHandler에 대한 이해가 필요합니다.&lt;/p&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;PartitionHandler&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface PartitionHandler {
    Collection&amp;lt;StepExecution&amp;gt; handle(StepExecutionSplitter stepSplitter, StepExecution stepExecution) throws Exception;
}&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;PartitionHandler 인터페이스는 매니저 (마스터) Step이 Worker Step를 어떻게 다룰지를 정의합니다. 예를 들면, Slave step으로 어떤 step을 두고 병렬로 실행할지, 병렬로 실행한다면 스레드풀 관리는 어떻게 할지, gridSize는 몇으로 둘지 등등을 비롯하여 모든 작업이 완료되었는지를 식별하는 역할을 하기도 합니다. 일반적으로 PartitionerHandler는 직접 구현하지 않고 제공되는 구현체를 사용합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TaskExecutorPartitionHandler
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 JVM 내에서 분할 개념을 사용할 수 있도록 같은 JVM 내에서 스레드로 분할 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MessageChannelPartitionHandler
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원격의 JVM에 메타 데이터를 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Partitioner&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Partitioner {
    Map&amp;lt;String, ExecutionContext&amp;gt; partition(int gridSize);
}&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;Partitioner는 파티셔닝 된 Step (Slave Step)을 위한 Step Executions을 생성하는 인터페이스입니다. 기본 구현체인 SimplePartitioner는 인자로 받은 gridSize 만큼 빈 StepExecutions를 생성합니다. 일반적으로는 StepExecution 1개당 1개의 Slave Step를 매핑하기 때문에 Slave Step의 수와 마찬가지로 봐도 무방합니다.(spring batch에서는 기본적으로 1:1로 매핑하기 때문에 이를 변경하려면 partitionHandler를 통해서 변경해야 합니다.) gridSize만 지정했다고 해서 Slave Step이 자동으로 구성되는 것은 아니기 때문에 gridSize를 이용하여 각 Slave Step마다 어떤 Step Executions 환경을 제공할지는 개발자가 직접 처리해야 합니다.&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;1172&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbgAvr/btsEnPyDei1/nMIYTYpwSd7kv6mJ6HkalK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbgAvr/btsEnPyDei1/nMIYTYpwSd7kv6mJ6HkalK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbgAvr/btsEnPyDei1/nMIYTYpwSd7kv6mJ6HkalK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbgAvr%2FbtsEnPyDei1%2FnMIYTYpwSd7kv6mJ6HkalK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1172&quot; height=&quot;633&quot; data-origin-width=&quot;1172&quot; data-origin-height=&quot;633&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 스레드는 같은 SlaveStep을 실행하지만, 서로 다른 StepExecution 정보를 가지고 수행됩니다. Partitioning은 StepScope를 지정하게 되는데 이에 따라 서로 같은 SlaveStep을 수행하게 되어 같은 프록시를 바라보지만 실제 실행할 때는 결과적으로 각 스레드마다 타겟 빈을 새로 만들기 때문에 서로 다른 타겟 빈을 바라보게 되어 동시성 이슈가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;br /&gt;특정 기간의 DB 데이터를 파티션 하는 파티셔너를 만들어 처리해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// https://github.com/spring-projects/spring-batch/blob/d8fc58338d3b059b67b5f777adc132d2564d7402/spring-batch-samples/src/main/java/org/springframework/batch/sample/common/ColumnRangePartitioner.java
// spring 공식 샘플 코드 참고
@Slf4j
@RequiredArgsConstructor
public class ProductIdRangePartitioner implements Partitioner {

    private final ProductRepository productRepository;
    private final LocalDate startDate;
    private final LocalDate endDate;

    @Override
    public Map&amp;lt;String, ExecutionContext&amp;gt; partition(int gridSize) {
        long min = productRepository.findMinId(startDate, endDate); // 기간 동안의 최소 pk
        long max = productRepository.findMaxId(startDate, endDate); // 기간 동안의 최대 pk
        long targetSize = (max - min) / gridSize + 1;

        Map&amp;lt;String, ExecutionContext&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        long number = 0;
        long start = min;
        long end = start + targetSize - 1;

        // min, max를 통해 처리해야할 총 데이터 수를 구하고 gridSize로 나눠
        // 각 slaveStep에서 처리해야할 데이터의 시작, 끝 pk를 구해 ExecutionContext에 put
        while (start &amp;lt;= max) {
            ExecutionContext value = new ExecutionContext();
            result.put(&quot;partition&quot; + number, value);

            if (end &amp;gt;= max) {
                end = max;
            }

            value.putLong(&quot;minId&quot;, start); // 각 파티션마다 사용될 minId
            value.putLong(&quot;maxId&quot;, end); // 각 파티션마다 사용될 maxId
            start += targetSize;
            end += targetSize;
            number++;
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 대상인 Product의 Repository (productRepository)를 통해 Job Parameter로 받은 시작일과 종료일로 전체 조회 대상의 맨 첫 PK값과 맨 끝 PK값을 가져옵니다. 그리고 이를 gridSize에 맞게 각 파티션 ExecutionContext으로 할당합니다. 예를 들어 2021.1.12 ~ 2021.1.13 기간에 해당하는 Product의 PK가 1부터 10까지 있다면 partition(5) (gridSize=5)를 수행 시 다음과 같은 결과가 리턴됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;partition0 (minId:1, maxId:2)
partition1 (minId:3, maxId:4)
partition2 (minId:5, maxId:6)
partition3 (minId:7, maxId:8)
partition4 (minId:9, maxId:10)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Configuration
public class PartitionLocalConfiguration {
    public static final String JOB_NAME = &quot;partitionSampleBatch&quot;;

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private final ProductRepository productRepository;
    private final ProductBackupRepository productBackupRepository;

    private int chunkSize = 100;
    private int poolSize = 100;

    @Bean(name = JOB_NAME)
    public Job job() {
        return jobBuilderFactory.get(JOB_NAME)
                .start(step1Manager())
                .build();
    }

    @Bean(name = JOB_NAME +&quot;_step1Manager&quot;)
    public Step step1Manager() {
        // partitionHandler를 지정할 수도 있지만 지정하지 않으면 TaskExecutorPartitionHandler가 기본으로 사용
        return stepBuilderFactory.get(&quot;step1.manager&quot;)
                .partitioner(&quot;step1&quot;, partitioner(null, null)) // 파티셔너 등록
                .step(step1()) // slave Step 등록
                .gridSize(poolSize) // StepExecution이 형성될 개수 = 파티션 되는 데이터 뭉텅이 수 = 스레드 풀 사이즈과 일치시키는게 좋음
                .taskExecutor(taskExecutor()) // MasterStep이 SlaveStep을 다루는 스레드 형성 방식
                .build();
    }

    // partitioning에서 사용할 taskExecutor 정의
    @Bean(name = JOB_NAME+&quot;taskPool&quot;)
    public TaskExecutor executor() {
      ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
      executor.setCorePoolSize(poolSize);
      executor.setMaxPoolSize(poolSize);
      executor.setThreadNamePrefix(&quot;partition-thread&quot;);
      executor.setWaitForTasksToCompleteOnShutdown(Boolean.TRUE);
      executor.initialize();
      return executor;
    }

    @Bean(name = JOB_NAME +&quot;_partitioner&quot;)
    @StepScope
    public ProductIdRangePartitioner partitioner(
            @Value(&quot;#{jobParameters['startDate']}&quot;) String startDate,
            @Value(&quot;#{jobParameters['endDate']}&quot;) String endDate) {
        LocalDate startLocalDate = LocalDate.parse(startDate, DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&quot;));
        LocalDate endLocalDate = LocalDate.parse(endDate, DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&quot;));

        return new ProductIdRangePartitioner(productRepository, startLocalDate, endLocalDate);
    }

    @Bean(name = JOB_NAME +&quot;_step&quot;)
    public Step step1() {
        return stepBuilderFactory.get(JOB_NAME +&quot;_step&quot;)
                .&amp;lt;Product, ProductBackup&amp;gt;chunk(chunkSize)
                .reader(reader(null, null))
                .processor(processor())
                .writer(writer())
                .build();
    }

    @Bean(name = JOB_NAME +&quot;_reader&quot;)
    @StepScope
    // 파티셔너를 통해 넣은 minId와 maxId값을 사용하여 reader의 sql params 구성
    public JpaPagingItemReader&amp;lt;Product&amp;gt; reader(
            @Value(&quot;#{stepExecutionContext[minId]}&quot;) Long minId,
            @Value(&quot;#{stepExecutionContext[maxId]}&quot;) Long maxId) {

        Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();
        params.put(&quot;minId&quot;, minId);
        params.put(&quot;maxId&quot;, maxId);

        log.info(&quot;reader minId={}, maxId={}&quot;, minId, maxId);

        return new JpaPagingItemReaderBuilder&amp;lt;Product&amp;gt;()
                .name(JOB_NAME +&quot;_reader&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(chunkSize)
                .queryString(
                        &quot;SELECT p &quot; +
                        &quot;FROM Product p &quot; +
                        &quot;WHERE p.id BETWEEN :minId AND :maxId&quot;)
                .parameterValues(params)
                .build();
    }

    private ItemProcessor&amp;lt;Product, ProductBackup&amp;gt; processor() {
        return ProductBackup::new;
    }

    @Bean(name = JOB_NAME +&quot;_writer&quot;)
    @StepScope
    public ItemWriter&amp;lt;ProductBackup&amp;gt; writer() {

        return items -&amp;gt; {
            productBackupRepository.saveAll(items);
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/72</guid>
      <comments>https://backtony.tistory.com/72#entry72comment</comments>
      <pubDate>Sun, 4 Feb 2024 15:21:59 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - Scope</title>
      <link>https://backtony.tistory.com/71</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;@JobScope와 @StepScope&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scope는 스프링 컨테이너에서 빈이 관리되는 범위를 의미합니다. JobScope와 StepScope는 Job과 Step의 빈 생성과 실행에 관여하는 스코프입니다. &lt;b&gt;프록시 모드를&lt;/b&gt; 기본값으로 하기 때문에 애플리케이션 구동 시점에는 프록시 빈이 생성되고 실행 시점에 실제 빈 생성이 이뤄집니다. 이를 통해 빈의 실행 시점에 값을 참조할 수 있는 일종의 Lazy Binding이 가능해집니다. 스코프를 사용하게 되면 @Value를 사용하여 아래와 같이 인자로 주입받을 수 있게 됩니다.&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;@Value(&quot;#{jobParameters[파라미터명]}&quot;)&lt;/li&gt;
&lt;li&gt;@Value(&quot;#{jobExecutionContext[파라미터명]}&quot;)&lt;/li&gt;
&lt;li&gt;@Value(&quot;#{stepExecutionContext[파라미터명]}&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아키텍처&lt;/h2&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;1274&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLysSr/btsEoqZCMez/pyn1rTFFnkpnH7EPBWPiAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLysSr/btsEoqZCMez/pyn1rTFFnkpnH7EPBWPiAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLysSr/btsEoqZCMez/pyn1rTFFnkpnH7EPBWPiAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLysSr%2FbtsEoqZCMez%2Fpyn1rTFFnkpnH7EPBWPiAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1274&quot; height=&quot;420&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;420&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@JobScope가 붙어서 프록시로 생성된 Step에 요청이 들어옵니다.&lt;/li&gt;
&lt;li&gt;프록시는 JobScope의 JobContext에서 실제 타겟 빈이 존재하는지 확인합니다.&lt;/li&gt;
&lt;li&gt;있으면 찾아서 반환합니다.&lt;/li&gt;
&lt;li&gt;없으면 빈 팩토리에서 실제 Step빈을 생성하고 JobContext에 담고 이를 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 객체 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@JobScope, @StepScope 애노테이션이 붙은 빈 선언은 내부적으로 프록시 빈 객체가 생성되어 등록됩니다.&lt;/li&gt;
&lt;li&gt;Job 실행 시 Proxy 객체가 실제 빈을 호출해서 해당 메서드를 실행시키는 구조입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JobScope, StepScope
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Proxy 객체의 실제 대상이 되는 Bean을 등록, 해제하는 역할을 하는 클래스입니다.&lt;/li&gt;
&lt;li&gt;실제 대상이 되는 빈을 저장하고 있는 JobContext, StepContext를 갖고 있습니다.&lt;/li&gt;
&lt;li&gt;Job의 실행 시점에 프록시 객체는 실제 빈을 찾기 위해서 JobScope, StepScope의 JobContext, StepContext를 찾게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;scope를 통해 인자를 주입받는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표현식 언어를 통해 유연하고 편리하게 주입받아 파라미터로 사용할 수 있게 됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StepContribution에서 일일이 원하는 값을 꺼내서 사용하지 않아도 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step 빈 생성이 구동시점이 아닌 런타임 시점에 생성되어 객체의 지연 로딩이 가능해집니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 덕분에 위에 표현식을 사용할 수 있는 것입니다.&lt;/li&gt;
&lt;li&gt;표현식으로 작성한 값들은 컴파일 시점에 존재하지 않고 런타임 시점에 채워지면서 존재하게 되는 값입니다.&lt;/li&gt;
&lt;li&gt;만약 빈이 애플리케이션 로딩 시점에 만들어진다면 DI를 해야 하는데 해당 값들이 현재 존재하지 않기 때문에 찾을 수가 없습니다.&lt;/li&gt;
&lt;li&gt;하지만 런타임 시점에 빈을 만들게 되면 값을 다 받아놓고(표현식에 명시한 값들) 빈을 만들기 때문에 주입이 가능하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;병렬 처리 시에 각 스레드마다 Step 객체가 생성되어 할당되기 때문에 Tasklet에 멤버 변수가 존재해도 동시성에 문제가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 런타임 시에 주입받는 JobParameters를 손쉽게 처리하기 위한 목적으로 사용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JobScope&lt;/h2&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;Step 선언문에 붙입니다.&lt;/li&gt;
&lt;li&gt;@Value로 JobParameter과 JobExectionContext만 사용 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;StepScope&lt;/h2&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;Tasklet이나 ItemReader, ItemWriter, ItemProcessor 선언문에 붙입니다.&lt;/li&gt;
&lt;li&gt;@Value로 JobParameter, JobExecutionContext, StepExecutionContext 사용 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class Test2Config {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1(null)) // 런타임시 주입받을 것이므로 현재는 null로 주입
                .listener(new CustomJobListener())
                .build();
    }

    @Bean
    @JobScope
    public Step step1(@Value(&quot;#{jobParameters['message']}&quot;) String message) {
        System.out.println(&quot;message = &quot; + message);
        return stepBuilderFactory.get(&quot;step1&quot;)
                .tasklet(tasklet(null,null)) // 런타임 시 주입되므로 null 
                .listener(new CustomStepListener())
                .build();
    }

    @Bean
    @StepScope
    public Tasklet tasklet(@Value(&quot;#{jobExecutionContext['name']}&quot;) String name,
                           @Value(&quot;#{stepExecutionContext['name2']}&quot;) String name2){
        return (stepContribution, chunkContext) -&amp;gt; {
            System.out.println(&quot;name = &quot; + name);
            System.out.println(&quot;name2 = &quot; + name2);
            return RepeatStatus.FINISHED;
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomJobListener implements JobExecutionListener {
    @Override
    public void beforeJob(JobExecution jobExecution) {
        jobExecution.getExecutionContext().putString(&quot;name&quot;,&quot;user1&quot;);
    }

    @Override
    public void afterJob(JobExecution jobExecution) {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomStepListener implements StepExecutionListener {
    @Override
    public void beforeStep(StepExecution stepExecution) {
        stepExecution.getExecutionContext().putString(&quot;name2&quot;,&quot;user2&quot;);
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Value를 통해서 주입되는 값들을 런타임 시에 제공하기 때문에 컴파일 시점에 에러를 없애기 위해 null로 값을 채워줘야 합니다. 리스너를 통해서 name, name2 값을 넣어주었고, 실행 시점에 intellij IDE의 Configuration을 통해서 arguments로 message=message로 주고 실행시키면 주입한 값이 정상적으로 찍히게 됩니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>scope</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/71</guid>
      <comments>https://backtony.tistory.com/71#entry71comment</comments>
      <pubDate>Sun, 4 Feb 2024 12:38:05 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - 리스너</title>
      <link>https://backtony.tistory.com/70</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Listener&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 흐름 중에 Job, Step, Chunk 단계의 실행 전후에 발생하는 이벤트를 받아 용도에 맞게 활용할 수 있도록 제공하는 인터셉터 개념의 클래스입니다. 각 단계별로 로그기록을 남기거나 소요된 시간을 계산하거나 실행상태 정보들을 참조 및 조회할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyOYZa/btsElljjHV2/66u14MRarlWjEkQZGVwbw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyOYZa/btsElljjHV2/66u14MRarlWjEkQZGVwbw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyOYZa/btsElljjHV2/66u14MRarlWjEkQZGVwbw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyOYZa%2FbtsElljjHV2%2F66u14MRarlWjEkQZGVwbw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1627&quot; height=&quot;653&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;653&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobExecutionListener : Job 실행 전후&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StepExecutionListener : Step 실행 전후&lt;/li&gt;
&lt;li&gt;ChunkListener : Chunk 실행 전후(Tasklet 실행 전후), 오류 시점&lt;/li&gt;
&lt;li&gt;ItemReaderListener : ItemReader 실행 전후, 오류 시점, 단, item이 null일 경우에는 호출 X&lt;/li&gt;
&lt;li&gt;ItemProcessorListener : ItemProcessor 실행 전후, 오류 시점, 단, item이 null일 경우에는 호출 X&lt;/li&gt;
&lt;li&gt;ItemWriterListener : ItemWriter 실행 전후, 오류 시점, 단, item이 null일 경우에는 호출 X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SkipListener : item 처리가 Skip 될 경우 Skip된 item을 추적&lt;/li&gt;
&lt;li&gt;RetryListener : Retry 시작, 종료, 에러 시점&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JobExecutionListener / StepExecutionListener&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;listener를 등록하는 방식은 인터페이스를 구현하거나 애노테이션을 사용하는 방식이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .incrementer(new RunIdIncrementer())
                .start(step())
                .listener(new CustomJobExecutionListener())
                //.listener(new CustomJobAnnotationExecutionListener()) // 애노테이션 방식
                .build();
    }


    @Bean
    public Step step() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer2&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(items -&amp;gt; System.out.println(&quot;items = &quot; + items))
                .listener(new CustomStepExecutionListener())
                .build();
    }


    @Bean
    public JpaPagingItemReader&amp;lt;Customer&amp;gt; customItemReader() {
        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;customItemReader&quot;)
                .pageSize(chunkSize)
                .entityManagerFactory(entityManagerFactory)
                .queryString(&quot;select c from Customer c order by c.id&quot;)
                .build();

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomJobExecutionListener implements JobExecutionListener {
    @Override
    public void beforeJob(JobExecution jobExecution) {
        System.out.println(&quot;job name : &quot; + jobExecution.getJobInstance().getJobName() + &quot; start&quot;);
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        String jobName = jobExecution.getJobInstance().getJobName();
        long startTime = jobExecution.getStartTime().getTime();
        long endTime = jobExecution.getEndTime().getTime();
        long executionTime = endTime - startTime;
        System.out.println(&quot;job name : &quot; + jobName  + &quot; end &quot;+ &quot; execution time : &quot;+executionTime);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class CustomStepExecutionListener implements StepExecutionListener {
    @Override
    public void beforeStep(StepExecution stepExecution) {
        String stepName = stepExecution.getStepName();
        System.out.println(&quot;stepName = &quot; + stepName+ &quot; start&quot;);
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        String stepName = stepExecution.getStepName();
        ExitStatus exitStatus = stepExecution.getExitStatus();
        System.out.println(&quot;stepName = &quot; + stepName + &quot; end &quot; + &quot; exitStatus : &quot;+ exitStatus);
        // exitStatus 조작 가능
        //return ExitStatus.FAILED
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 인터페이스를 구현해서 원하는 로직을 작성하면 됩니다. StepListener의 반환값으로 ExitStatus를 수정해서 Job의 ExitStatus에 반영되는 값을 수정할 수 있습니다. 아래 코드는 인터페이스를 구현하지 않고 애노테이션으로 리스너를 작성한 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomJobAnnotationExecutionListener {

    @BeforeJob
    public void beforeJob(JobExecution jobExecution) {
        System.out.println(&quot;job name : &quot; + jobExecution.getJobInstance().getJobName() + &quot; start&quot;);
    }

    @AfterJob
    public void afterJob(JobExecution jobExecution) {
        String jobName = jobExecution.getJobInstance().getJobName();
        long startTime = jobExecution.getStartTime().getTime();
        long endTime = jobExecution.getEndTime().getTime();
        long executionTime = endTime - startTime;
        System.out.println(&quot;job name : &quot; + jobName  + &quot; end : &quot;+ &quot; execution time : &quot;+executionTime);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 리스너를 등록하는 방식은 똑같고 구현하는 방식만 애노테이션으로 변경된 것입니다. 애노테이션 방식은 인터페이스를 구현하지 않고 애노테이션으로 언제 동작하는지 명시하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ChunkListener / ItemReadListener / ItemProcessorListener / ItemWriterListener&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크 리스너는 청크 주기마다 호출됩니다. 즉, reader - writer 하나의 싸이클 마다 호출됩니다. 네 가지 리스너 모두 애노테이션 방식을 지원합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .incrementer(new RunIdIncrementer())
                .start(step())
                .build();
    }

    @Bean
    public Step step() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer2&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor())
                .writer(customItemWriter())
                .listener(new CustomChunkListener())
                .listener(new CustomItemReadListener())
                .listener(new CustomItemProcessorListener())
                .listener(new CustomItemWriterListener())
                .build();
    }


    @Bean
    public JpaPagingItemReader&amp;lt;Customer&amp;gt; customItemReader() {
        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;customItemReader&quot;)
                .pageSize(chunkSize)
                .entityManagerFactory(entityManagerFactory)
                .queryString(&quot;select c from Customer c order by c.id&quot;)
                .build();

    }
    @Bean
    public ItemProcessor&amp;lt;? super Customer, ? extends Customer2&amp;gt; customItemProcessor() {
        return item -&amp;gt; {
            return new Customer2(item.getName(), item.getAge());
        };
    }
    @Bean
    public ItemWriter&amp;lt;? super Customer2&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            System.out.println(&quot;items = &quot; + items);
        };
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomChunkListener implements ChunkListener {
    private int count;

    @Override
    public void beforeChunk(ChunkContext context) {
        count++;
        System.out.println(&quot;before chunk : &quot;+ count);
    }

    @Override
    public void afterChunk(ChunkContext context) {
        System.out.println(&quot;after chunk : &quot;+ count);
    }

    @Override
    public void afterChunkError(ChunkContext context) {
        System.out.println(&quot;error chunk : &quot;+ count);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomItemReadListener implements ItemReadListener {
    private int count;

    @Override
    public void beforeRead() {
        count++;
        System.out.println(&quot;before reader : &quot;+ count);
    }

    @Override
    public void afterRead(Object item) {
        System.out.println(&quot;after reader : &quot;+ count);

    }

    @Override
    public void onReadError(Exception ex) {
        System.out.println(&quot;error reader : &quot;+ count);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomItemProcessorListener implements ItemProcessListener&amp;lt;Customer, Customer2&amp;gt; {
    private int count;

    @Override
    public void beforeProcess(Customer item) {
        count++;
        System.out.println(&quot;before processor : &quot;+ count);
    }

    @Override
    public void afterProcess(Customer item, Customer2 result) {
        System.out.println(&quot;after processor : &quot;+ count);

    }

    @Override
    public void onProcessError(Customer item, Exception e) {
        System.out.println(&quot;error processor : &quot;+ count);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CustomItemWriterListener implements ItemWriteListener&amp;lt;Customer2&amp;gt; {
    private int count;


    @Override
    public void beforeWrite(List&amp;lt;? extends Customer2&amp;gt; items) {
        count++;
        System.out.println(&quot;before writer : &quot;+ count);
    }

    @Override
    public void afterWrite(List&amp;lt;? extends Customer2&amp;gt; items) {
        System.out.println(&quot;after writer : &quot;+ count);

    }

    @Override
    public void onWriteError(Exception exception, List&amp;lt;? extends Customer2&amp;gt; items) {
        System.out.println(&quot;error writer : &quot;+ count);

    }
}&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;사용 방식은 전부 유사합니다. 인터페이스를 구현해서 로직을 작성하고 listener로 등록해주면 됩니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>listener</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/70</guid>
      <comments>https://backtony.tistory.com/70#entry70comment</comments>
      <pubDate>Thu, 1 Feb 2024 22:58:30 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - 반복 및 오류 제어</title>
      <link>https://backtony.tistory.com/69</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Repeat&lt;/h2&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;스프링 배치는 특정 조건이 충족될 때까지 Job 또는 Step을 반복하도록 배치 애플리케이션을 구성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;스프링 배치에서는 Step과 Chunk의 반복을 RepeatOperation을 사용해 처리하고 있습니다.&lt;/li&gt;
&lt;li&gt;기본 구현체로 RepeatTemplate을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQfn81/btsEcqGXEet/tLTRX078JJbGDwgUTxU4sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQfn81/btsEcqGXEet/tLTRX078JJbGDwgUTxU4sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQfn81/btsEcqGXEet/tLTRX078JJbGDwgUTxU4sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQfn81%2FbtsEcqGXEet%2FtLTRX078JJbGDwgUTxU4sK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1513&quot; height=&quot;394&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step은 RepeatTemplate을 사용해 Tasklet을 반복적으로 실행합니다. ChunkOrientedTasklet은 내부적으로 ChunkProvider를 통해 ItemReader로 데이터를 읽어올 것을 지시합니다. ChunkProvider는 내부적으로 RepeatTemplate을 갖고 있고 이를 이용해 반복적으로 ItemReader에게 반복적으로 데이터를 읽어오도록 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FaultTolerant&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치는 Job 실행 중에 오류가 발생할 경우 장애를 처리하기 위한 기능을 제공합니다. 오류가 발생해도 Step이 즉시 종료되지 않고 Retry 혹은 Skip 기능을 활성화 함으로 내결함성 서비스가 가능합니다.&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;Skip
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemReader, ItemProcessor, ItemWriter에 적용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Retry
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemProcessor, ItemWriter에 적용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&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;1261&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kT5Up/btsEeH2lyrg/gTLi8kmpwrMcldGNhVELf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kT5Up/btsEeH2lyrg/gTLi8kmpwrMcldGNhVELf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kT5Up/btsEeH2lyrg/gTLi8kmpwrMcldGNhVELf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkT5Up%2FbtsEeH2lyrg%2FgTLi8kmpwrMcldGNhVELf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1261&quot; height=&quot;693&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Skip&lt;/h3&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;Skip은 데이터를 처리하는 동안 설정된 Exception이 발생했을 경우, 해당 데이터 처리를 건너뛰는 기능입니다.&lt;/li&gt;
&lt;li&gt;ItemReader, ItemProcessor, ItemWriter에 적용 가능합니다.&lt;/li&gt;
&lt;li&gt;데이터의 사소한 오류에 대해 Step의 실패처리 대신 Skip 함으로써, 배치수행의 빈번한 실패를 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1113&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SZORb/btsEeLDxEzU/Ri2ZYcmi5t2qaTXIrOoDzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SZORb/btsEeLDxEzU/Ri2ZYcmi5t2qaTXIrOoDzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SZORb/btsEeLDxEzU/Ri2ZYcmi5t2qaTXIrOoDzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSZORb%2FbtsEeLDxEzU%2FRi2ZYcmi5t2qaTXIrOoDzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1113&quot; height=&quot;396&quot; data-origin-width=&quot;1113&quot; data-origin-height=&quot;396&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;itemReader
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;item을 한건씩 읽다가 예외가 발생하게 되면 해당 item을 skip하고 다음 item을 읽습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;itemProcessor
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;itemProcessor는 item을 처리하다가 예외가 발생하면 해당 Chunk의 첫 단계로 돌아가서 itemReader로부터 다시 데이터를 받습니다.&lt;/li&gt;
&lt;li&gt;이때 itemReader에서 실제로 데이터를 다시 읽는 것은 아니고 캐시에 저장한 아이템을 다시 사용해서 itemProcessor로 다시 보내줍니다.&lt;/li&gt;
&lt;li&gt;itemProcessor는 다시 아이템들을 받아서 실행하게 되는데 도중에 이전에 실행에서 예외가 발생했던 정보가 내부적으로 남아있기 때문에 위의 그림처럼 item2의 차례가 오면 처리하지 않고 넘어갑니다.&lt;/li&gt;
&lt;li&gt;결론적으로 skip하는 건 맞는데 itemReader와 동작 방식이 다릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;itemWriter
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 그림에서 Writer에서 item4번에서 예외가 발생했다면 다시 Chunk 단위로 ItemReader로 돌아갑니다.&lt;/li&gt;
&lt;li&gt;캐싱된 데이터로 itemReader는 itemProcessor로 넘깁니다.&lt;/li&gt;
&lt;li&gt;itemProcessor는 이전처럼 청크 단위만큼 item을 처리하고 한 번에 writer로 넘기는 게 아니라 단건 처리 후 writer로 단건을 넘깁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 : itemReader Skip&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 5;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(items -&amp;gt; System.out.println(&quot;items = &quot; + items))
                .faultTolerant()
                .skip(SkippableException.class)                
                .skipLimit(4)
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {
        return new ItemReader&amp;lt;String&amp;gt;() {
            int i = 0;

            @Override
            public String read() throws SkippableException {
                i++;
                if (i==3){
                    throw new SkippableException(&quot;skip exception&quot;);
                }
                System.out.println(&quot;itemReader : &quot; + i);
                return i &amp;gt; 20 ? null : String.valueOf(i);
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번째 데이터를 읽을 때 SkippableException 예외가 터지지만 4번까지 허용하므로 skip하고 진행됩니다. 청크 사이즈가 5이기 때문에 첫 번째 읽기 작업에서는 1,2,4,5,10 이 다음 작업으로 넘어갑니다. skip에 체이닝으로 .skip을 연달아서 사용하여 여러 개의 Exception을 등록할 수도 있습니다.&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;예시 : itemProcessor Skip&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 5;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor1())
                .writer(items -&amp;gt; System.out.println(&quot;items = &quot; + items))
                .faultTolerant()
                .skip(SkippableException.class)
                .skipLimit(3)
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {
        return new ItemReader&amp;lt;String&amp;gt;() {
            int i = 0;

            @Override
            public String read() throws SkippableException {
                i++;
                System.out.println(&quot;itemReader : &quot; + i);
                return i &amp;gt; 5 ? null : String.valueOf(i);
            }
        };
    }

    @Bean
    public ItemProcessor&amp;lt;? super String, String&amp;gt; customItemProcessor1() {
        return item -&amp;gt; {
            System.out.println(&quot;itemProcessor &quot; + item);

            if (item.equals(&quot;3&quot;)) {
                throw new SkippableException(&quot;Process Failed &quot;);

            }
            return item;
        };
    }

}
---------------------------------------------------------------------------
// 출력
itemReader : 1
itemReader : 2
itemReader : 3
itemReader : 4
itemReader : 5
itemProcessor 1
itemProcessor 2
itemProcessor 3
itemProcessor 1
itemProcessor 2
itemProcessor 4
itemProcessor 5
items = [1, 2, 4, 5]
itemReader : 10&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;itemProcessor 부분에서 3번째 아이템에서 예외가 발생합니다. itemReader는 캐싱된 데이터를 읽어서 다시 itemProcessor로 넘기기 때문에 출력이 찍히지 않습니다.&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;예시 : itemWriter Skip&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 5;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor1())
                .writer(customItemWriter())
                .faultTolerant()
                .skip(SkippableException.class)
                .skipLimit(3)
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {
        return new ItemReader&amp;lt;String&amp;gt;() {
            int i = 0;

            @Override
            public String read() throws SkippableException {
                i++;
                System.out.println(&quot;itemReader : &quot; + i);
                return i &amp;gt; 5 ? null : String.valueOf(i);
            }
        };
    }

    @Bean
    public ItemProcessor&amp;lt;? super String, String&amp;gt; customItemProcessor1() {
        return item -&amp;gt; {
            System.out.println(&quot;itemProcessor &quot; + item);
            return item;
        };
    }

    @Bean
    public ItemWriter&amp;lt;? super String&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            for (String item : items) {
                if (item.equals(&quot;4&quot;)){
                    throw new SkippableException(&quot;4&quot;);
                }
            }
            System.out.println(&quot;items = &quot; + items);
        };
    }


}
------------------------------------------------------------
// 출력 결과
itemReader : 1
itemReader : 2
itemReader : 3
itemReader : 4
itemReader : 5
itemProcessor 1
itemProcessor 2
itemProcessor 3
itemProcessor 4
itemProcessor 5
itemProcessor 1
items = [1]
itemProcessor 2
items = [2]
itemProcessor 3
items = [3]
itemProcessor 4
itemProcessor 5
items = [5]
itemReader : 10&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;예외가 발생하고 난 후 itemProcessor는 itemWriter로 리스트가 아니라 한건씩만 보내서 처리하고 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Retry&lt;/h3&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;ItemProcessor, ItemWriter에서 설정된 Exception이 발생했을 때, 지정한 정책에 따라 데이터 처리를 재시도하는 기능입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ItemReader에서는 지원하지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예외 발생 시 재시도 설정에 의해서 해당 Chunk의 처음부터 다시 시작합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Retry Count는 Item마다 각각 가지고 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;RetryLimit 횟수 이후에도 재시도가 실패한다면 &lt;b&gt;recover&lt;/b&gt; 에서 후속작업을 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1109&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRIA4w/btsEf1MVnv4/1VntWZntiXGLhQe1IJ7KE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRIA4w/btsEf1MVnv4/1VntWZntiXGLhQe1IJ7KE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRIA4w/btsEf1MVnv4/1VntWZntiXGLhQe1IJ7KE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRIA4w%2FbtsEf1MVnv4%2F1VntWZntiXGLhQe1IJ7KE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1109&quot; height=&quot;398&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 itemProcessor와 itemWriter는 ChunkProcessor에서 실행이 되었지만, Retry 기능이 활성화되면 RetryTemplate 안에서 ItemProcessor와 itemWriter가 실행됩니다. 예외가 발생하면 RetryTemplate 안에서 처리가 진행이 됩니다. itemProcessor에서 예외가 발생하면 다시 Chunk 단계의 처음부터 시작합니다. skip과 마찬가지로 itemReader는 캐시에 저장된 값을 itemProcessor에 전달합니다. itemWriter에서 skip의 경우에는 단건 처리로 변경되었지만 retry의 경우에는 원래대로 다건 처리 형태가 유지됩니다.&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;예시 : retry Writer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 5;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor1())
                .writer(customItemWriter())
                .faultTolerant()
                .retry(RetryableException.class)
                .retryLimit(2)
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {
        return new ItemReader&amp;lt;String&amp;gt;() {
            int i = 0;

            @Override
            public String read() throws SkippableException {
                i++;
                System.out.println(&quot;itemReader : &quot; + i);
                return i &amp;gt; 5 ? null : String.valueOf(i);
            }
        };
    }


    @Bean
    public ItemProcessor&amp;lt;? super String, String&amp;gt; customItemProcessor1() {
        return item -&amp;gt; {
            System.out.println(&quot;itemProcessor : &quot; + item);

            return item;
        };
    }


    @Bean
    public ItemWriter&amp;lt;? super String&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            for (String item : items) {
                if (item.equals(&quot;4&quot;)){
                    throw new RetryableException(&quot;4&quot;);
                }
            }
            System.out.println(&quot;items = &quot; + items);
        };
    }
}
------------------------------------------------
// 출력 결과
itemReader : 1
itemReader : 2
itemReader : 3
itemReader : 4
itemReader : 5
itemProcessor : 1
itemProcessor : 2
itemProcessor : 3
itemProcessor : 4
itemProcessor : 5
itemProcessor : 1 // retryCount 1
itemProcessor : 2
itemProcessor : 3
itemProcessor : 4
itemProcessor : 5 
itemProcessor : 1 // retryCount 2 이제 더이상 재시작 못함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;itemWriter 4번째 Item을 처리할 때 예외가 터지게 되지만 retry 옵션에 의해 재시도하게 됩니다. itemReader에서는 캐싱한 데이터를 사용하기에 콘솔에 찍히지 않습니다. Writer에서 예외로 재시작되어도 Processor에서 한 개씩 보내지 않고 List로 한 번에 보내서 처리하게 됩니다. retryLimit이 2이므로 2번 재시작이 가능하고 3세트 진행 도중에 retryLimit 범위를 넘어가기 때문에 예외가 발생합니다.&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;예시 : retry Processor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 5;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor1())
                .writer(customItemWriter())
                .faultTolerant()
                .retry(RetryableException.class)
                .retryLimit(2)
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {
        return new ItemReader&amp;lt;String&amp;gt;() {
            int i = 0;

            @Override
            public String read() throws SkippableException {
                i++;
                System.out.println(&quot;itemReader : &quot; + i);
                return i &amp;gt; 5 ? null : String.valueOf(i);
            }
        };
    }

    @Bean
    public ItemProcessor&amp;lt;? super String, String&amp;gt; customItemProcessor1() {
        return item -&amp;gt; {


            if (item.equals(&quot;4&quot;)) {
                throw new RetryableException(&quot;Process Failed &quot;);
            }
            System.out.println(&quot;itemProcessor : &quot; + item);

            return item;
        };
    }

    @Bean
    public ItemWriter&amp;lt;? super String&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            System.out.println(&quot;items = &quot; + items);
        };
    }
}
-------------------------------------------
// 출력 결과
itemReader : 1
itemReader : 2
itemReader : 3
itemReader : 4
itemReader : 5
itemProcessor : 1
itemProcessor : 2
itemProcessor : 3
itemProcessor : 1 // retryCount 1
itemProcessor : 2
itemProcessor : 3
itemProcessor : 1 // retryCount 2
itemProcessor : 2
itemProcessor : 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;itemProcessor에서는 4번째 Item을 처리할 때 예외가 터지게 되지만 retry 옵션에 의해 재시도 하게 됩니다. itemReader에서는 캐싱한 데이터를 사용하기에 콘솔에 찍히지 않습니다. 결과적으로 3세트 진행 도중에 retryLimit 범위를 넘어가기 때문에 예외가 발생합니다.&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;예시 : retry + skip&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시들처럼 예외가 발생했을 때 해당 아이템을 Skip하고 재시도하고 싶을 수 있습니다. 이때는 Skip과 함께 사용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public Step step1() {
    return stepBuilderFactory.get(&quot;step&quot;)
            .&amp;lt;String, String&amp;gt;chunk(chunkSize)
            .reader(customItemReader())
            .processor(customItemProcessor1())
            .writer(customItemWriter())
            .faultTolerant()
            .retry(RetryableException.class)
            .retryLimit(2)
            .skip(RetryableException.class)
            .skipLimit(2)
            .build();
}

// 출력
itemReader : 1
itemReader : 2
itemReader : 3
itemReader : 4
itemReader : 5
itemProcessor : 1
itemProcessor : 2
itemProcessor : 3
itemProcessor : 1 // retryCount 1
itemProcessor : 2
itemProcessor : 3
itemProcessor : 1 // retryCount 2
itemProcessor : 2
itemProcessor : 3 // 4에서 3번째 예외가 터지면 recover로 skip되고 다음 item으로 넘어간다. -&amp;gt; skipLimit Count 1
itemProcessor : 5
items = [1, 2, 3, 5]&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;바로 위의 예시코드에서 step1에 skip관련 코드만 추가한 코드입니다. itemProcessor 처리과정에서 아이템 4번에서 예외가 발생했습니다. retryLimit이 2이므로 2번의 RetryableException가 허용되어 2세트 동안 재시작 처리되고 3세트에서는 여전히 4번째에서 예외가 터져서 종료되는 게 정상입니다. &lt;b&gt;하지만 위 코드에서는 RetryableException가 2번 터지고 3세트에서 예외가 터지면 recover 코드로 들어가 skip이 동작합니다.&lt;/b&gt; recover코드로 진입하여 여기서 해당 item을 skip 처리하고 skipCount를 1올리고 해당 item을 제외하고 바로 다음 처리로 넘어갑니다. 따라서, 1,2,3,4,5가 아니라 1,2,3,5를 처리하게 됩니다. 만약 Writer에서 2번 예외가 발생해서 3회차에 skip처리까지 온다면 3회차 과정에서는 writer 일괄 처리 없이 processor 1개 처리, writer 1개 처리하는 방식으로 진행됩니다.&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;예시 : item마다 갖는 retry Count&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 5;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor1())
                .writer(items -&amp;gt; System.out.println(&quot;items = &quot; + items))
                .faultTolerant()
                .retry(RetryableException.class)
                .retryLimit(2)
                .skip(RetryableException.class)
                .skipLimit(4)
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {
        return new ItemReader&amp;lt;String&amp;gt;() {
            int i = 0;

            @Override
            public String read() throws RetryableException {
                i++;
                return i &amp;gt; 5 ? null : String.valueOf(i);
            }
        };
    }

    @Bean
    public ItemProcessor&amp;lt;? super String, String&amp;gt; customItemProcessor1() {
        return item -&amp;gt; {

          System.out.println(&quot;itemProcessor : &quot; + item);
            if (item.equals(&quot;2&quot;) || item.equals(&quot;4&quot;)) {
                throw RetryableException(&quot;Process Failed &quot;);
            } else {
              System.out.println(&quot;itemProcessor : &quot; + item);

              return item;
            }
        };
    }
}
--------------------------------
// 출력 결과
itemReader = 1
itemReader = 2
itemReader = 3
itemReader = 4
itemReader = 5
itemProcessor : 1
itemProcessor : 1 // item 2 재시도 1회
itemProcessor : 1 // item 2 재시도 2회 , 재시도 횟수 끝 -&amp;gt; skip 처리 (1회)
itemProcessor : 3
itemProcessor : 1 // item 4 재시도 1회
itemProcessor : 3
itemProcessor : 1 // item 4 재시도 2회 
itemProcessor : 3 
itemProcessor : 5 // item 4 재시도 횟수 끝 -&amp;gt; skip 처리 (2회)
items : [1, 3, 5]        &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;위 코드는 item이 2,4일 경우 RetryableException이 발생합니다. 즉, RetryableException이 retryLimit으로 작성한 2보다 더 많이 발생합니다. 하지만 Retry Count는 item마다 갖고 있기 때문에 item마다 카운트 됩니다. 따라서 item 2는 재시도 2회 후 skip 처리되고, item 4보다 재시도 2회 후 skip 처리됩니다. 결과적으로 skip 카운트는 최대 4인데 2회만 발생하므로 정상 종료됩니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>FaultTolerant</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/69</guid>
      <comments>https://backtony.tistory.com/69#entry69comment</comments>
      <pubDate>Wed, 31 Jan 2024 23:31:18 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - ItemProcessor</title>
      <link>https://backtony.tistory.com/68</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemProcessor&lt;/h2&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;733&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bthUC7/btsD0IutdtD/NOK0Kdbs0zcGS85FCdyZX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bthUC7/btsD0IutdtD/NOK0Kdbs0zcGS85FCdyZX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bthUC7/btsD0IutdtD/NOK0Kdbs0zcGS85FCdyZX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbthUC7%2FbtsD0IutdtD%2FNOK0Kdbs0zcGS85FCdyZX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;733&quot; height=&quot;182&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;182&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터를 출력하기 전에 데이터를 가공 및 필터링 역할을 하는 인터페이스입니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ItemReader 및 ItemWriter와 분리되어 비즈니스 로직을 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;ItemReader로부터 받은 아이템을 특정 타입으로 변환해서 ItemWriter에 넘겨줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;Itemreader로부터 받은 아이템들 중 필터과정을 거쳐서 원하는 아이템들만 ItemWriter로 넘겨줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet 실행 시 선택적 요소기 때문에 필수 요소는 아닙니다.&lt;/li&gt;
&lt;li&gt;O process()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;I 제네릭은 ItemReader에서 받을 데이터 타입&lt;/li&gt;
&lt;li&gt;O 제네릭은 ItemWriter에게 보낼 데이터 타입&lt;/li&gt;
&lt;li&gt;아이템을 하나씩 가공 처리하며 null을 리턴할 경우 해당 아이템은 Chunk &amp;lt;O&amp;gt;에 저장되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ItemStream을 구현하지 않고 거의 대부분 Customizing 해서 사용하기 때문에 기본적으로 제공되는 구현체가 적습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Null을 반환하면 해당 item은 ItemWriter로 전달되지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzRyi/btsD6vGBXug/NbMImrRnBR6Fqkc95l47kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzRyi/btsD6vGBXug/NbMImrRnBR6Fqkc95l47kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzRyi/btsD6vGBXug/NbMImrRnBR6Fqkc95l47kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzRyi%2FbtsD6vGBXug%2FNbMImrRnBR6Fqkc95l47kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1622&quot; height=&quot;334&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Processor는 대부분 직접 구현해서 사용하기 때문에 Writer와 Reader에 비해 상대적으로 적은 구현체들을 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CompositeItemProcessor&lt;/h2&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;1530&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m3lcj/btsD4mwON6T/53YDlkmCtq3gUhGhAxvLu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m3lcj/btsD4mwON6T/53YDlkmCtq3gUhGhAxvLu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m3lcj/btsD4mwON6T/53YDlkmCtq3gUhGhAxvLu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm3lcj%2FbtsD4mwON6T%2F53YDlkmCtq3gUhGhAxvLu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1530&quot; height=&quot;440&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;ItemProcessor들을 연결(Chaining)해서 위임하면 각 ItemProcessor를 실행시킵니다. 이전의 ItemProcessor 반환 값은 다음 ItemProcessor 값으로 연결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&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;1022&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/begRTf/btsD3I7UWWZ/qkCGyKkTYliJVBO52EdFr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/begRTf/btsD3I7UWWZ/qkCGyKkTYliJVBO52EdFr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/begRTf/btsD3I7UWWZ/qkCGyKkTYliJVBO52EdFr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbegRTf%2FbtsD3I7UWWZ%2FqkCGyKkTYliJVBO52EdFr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1022&quot; height=&quot;291&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor())
                .writer(items -&amp;gt; {
                    for (String item : items) {
                        System.out.println(&quot;item = &quot; + item);
                    }
                })
                .build();
    }

    // 구성하는 프로세서 제네릭이 일치하므로 반환값 제네릭 사용 가능
    // 일치하지 않으면 제네릭 사용 불가
    @Bean
    public ItemProcessor&amp;lt;String, String&amp;gt; customItemProcessor() {
        List&amp;lt;ItemProcessor&amp;gt; processorList = new ArrayList&amp;lt;&amp;gt;();
        processorList.add(new CustomItemProcessor1());
        processorList.add(new CustomItemProcessor2());

        CompositeItemProcessor processor = new CompositeItemProcessor&amp;lt;&amp;gt;();
        processor.setDelegates(processorList);

        return processor;
    }


    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {
        return new ItemReader&amp;lt;String&amp;gt;() {
            int i = 0;

            @Override
            public String read() {
                i++;
                return i &amp;gt; 10 ? null : &quot;item&quot;;
            }
        };
    }

}
-------------------------------------------------------
public class CustomItemProcessor1 implements ItemProcessor&amp;lt;String,String&amp;gt; {

    @Override
    public String process(String item) throws Exception {
        return item + &quot; processor1&quot;;
    }
}
-------------------------------------------------------
public class CustomItemProcessor2 implements ItemProcessor&amp;lt;String,String&amp;gt; {
    @Override
    public String process(String item) throws Exception {
        return item + &quot; processor2&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 넘어온 아이템에 processor 번호를 붙이는 processor 2개를 만들고 CompositeItemProcessor를 사용해서 묶어서 하나의 Processor로 전달했습니다. CompositeItemProcessor를 구성하는 프로세서가 같은 제네릭 타입을 갖기 때문에 customItemProcessor 메서드의 반환값을 제네릭으로 세팅할 수 있었지만 구성하는 프로세서가 다른 제네릭 타입을 갖는다면 반환값에 제네릭을 세팅할 수 없습니다. 반환값이 다르다면 아래와 같이 세팅하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;@Bean
public CompositeItemProcessor compositeProcessor() {
    List&amp;lt;ItemProcessor&amp;gt; delegates = new ArrayList&amp;lt;&amp;gt;(2);
    delegates.add(processor1());
    delegates.add(processor2());

    CompositeItemProcessor processor = new CompositeItemProcessor&amp;lt;&amp;gt;();

    processor.setDelegates(delegates);

    return processor;
}&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;CompositeItemProcessor를 제외한 제공되는 다른 processor는 거의 사용할 일이 없고 보통 custom 하게 직접 만들어서 사용합니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ItemProcessor</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/68</guid>
      <comments>https://backtony.tistory.com/68#entry68comment</comments>
      <pubDate>Sun, 28 Jan 2024 16:19:47 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - ItemWriter</title>
      <link>https://backtony.tistory.com/67</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemWriter&lt;/h2&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;710&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U2WKg/btsD28F2hBC/u3AfI7zMNTgVCdPV7GeZ1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U2WKg/btsD28F2hBC/u3AfI7zMNTgVCdPV7GeZ1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U2WKg/btsD28F2hBC/u3AfI7zMNTgVCdPV7GeZ1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU2WKg%2FbtsD28F2hBC%2Fu3AfI7zMNTgVCdPV7GeZ1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;160&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;160&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Chunk 단위로 데이터를 받아 일괄 출력 작업을 위한 인터페이스입니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플랫 파일 - csv, txt&lt;/li&gt;
&lt;li&gt;XML, Jsono&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;Message Queuing 서비스&lt;/li&gt;
&lt;li&gt;Mail Service&lt;/li&gt;
&lt;li&gt;Custom reader&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다수의 구현체들이 itemReader와 같은 맥락으로 itemWriter와 ItemStream을 동시에 구현하고 있습니다.&lt;/li&gt;
&lt;li&gt;하나의 아이템이 아닌 아이템 리스트를 전달받아 수행합니다.&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet 실행 시 필수적 요소로 설정해야 합니다.&lt;/li&gt;
&lt;li&gt;void write()
&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;출력이 완료되고 트랜잭션이 종료되면 새로운 Chunk 단위 프로세스로 이동합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Yboc/btsD3nCYqFC/lUzLH4tvnme6PTqGzKtHt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Yboc/btsD3nCYqFC/lUzLH4tvnme6PTqGzKtHt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Yboc/btsD3nCYqFC/lUzLH4tvnme6PTqGzKtHt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Yboc%2FbtsD3nCYqFC%2FlUzLH4tvnme6PTqGzKtHt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1618&quot; height=&quot;558&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위와 같이 다양한 구현체들을 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JdbcBatchItemWriter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jdbc Batch 기능을 사용하여 Bulk insert/update/delete 방식으로 처리합니다. Bulk 처리로 단건 처리가 아닌 일괄 처리이기 때문에 성능에 이점을 갖습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&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;1541&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Haqsw/btsD3bvQvuf/2QDUiEO4KApeOuAY68xMl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Haqsw/btsD3bvQvuf/2QDUiEO4KApeOuAY68xMl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Haqsw/btsD3bvQvuf/2QDUiEO4KApeOuAY68xMl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHaqsw%2FbtsD3bvQvuf%2F2QDUiEO4KApeOuAY68xMl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1541&quot; height=&quot;478&quot; data-origin-width=&quot;1541&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapped는 둘 중에 하나만 사용할 수 있습니다. beanMapped는 ItemWriter로 넘어오는 객체의 필드 기반으로 sql 데이터 바인딩이 진행됩니다. columnMapped는 ItemWriter로 넘어오는 것이 객체가 아닌 Map 컬렉션으로 넘길 때 사용합니다. Map 컬렉션의 데이터들이 sql에 바인딩됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;Customer&amp;gt; customItemReader() {
        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;age&quot;,25);

        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jpaPagingItemReader&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(10)
                .queryString(&quot;select c from Customer c where age &amp;gt;= :age order by c.id&quot;)
                .parameterValues(parameters)
                .build();
    }


    @Bean
    public ItemWriter&amp;lt;Customer&amp;gt; customItemWriter() {
        return new JdbcBatchItemWriterBuilder&amp;lt;Customer&amp;gt;()
                .dataSource(dataSource)
                .sql(&quot;insert into customer2 values (:id, :age, :name)&quot;)
                .beanMapped()
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customer를 읽어서 이름만 다른 Customer2에 쓰는 작업입니다. beanMapped API를 사용하면 Customer 객체 기반으로 쿼리의 values에 파라미터가 알아서 매핑됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JpaItemWriter&lt;/h2&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;JPA 엔티티 기반으로 데이터를 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔티티를 하나씩&lt;/b&gt; 청크 크기만큼 insert 혹은 merge 한 다음 flush 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;insert 쿼리가 하나씩 나가기 때문에 JPA 벌크 insert를 하기 위해서는 rewriteBatchedStatements 옵션과 Table 전략 등 따로 세팅이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ItemReader나 ItemProcessor로부터 아이템을 전달받을 때는 &lt;b&gt;쓰기 할 엔티티 클래스 타입 자체를&lt;/b&gt; 받아야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&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;1345&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPX6WH/btsD3fkK0BX/wPcs31avtzACkNBZWti49k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPX6WH/btsD3fkK0BX/wPcs31avtzACkNBZWti49k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPX6WH/btsD3fkK0BX/wPcs31avtzACkNBZWti49k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPX6WH%2FbtsD3fkK0BX%2FwPcs31avtzACkNBZWti49k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1345&quot; height=&quot;352&quot; data-origin-width=&quot;1345&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;엔티티 자체를 바로 저장하기 때문에 쿼리문이 따로 필요 없습니다. spring batch에서 JpaItemWriter를 통한 write 작업은 신규 생성되는 Entity를 저장하는 기능과 기존 Entity의 값을 변경하는 두 방식에 대해 모두 대응해야 하기 때문에 Merge가 기본값으로 들어가면서 usePersist의 기본값은 false입니다. 따라서 JpaItemWriter가 save 하는 시점에는 select를 먼저 호출하고 insert를 수행하게 됩니다. 즉, 신규 Entity를 만들어내는 Job이라면 불필요한 select문을 줄이기 위해 usePersist 옵션을 true로 지정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer2&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor())
                .writer(customItemWriter())
                .build();
    }



    @Bean
    public ItemReader&amp;lt;Customer&amp;gt; customItemReader() {
        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;age&quot;,25);

        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jpaPagingItemReader&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(10)
                .queryString(&quot;select c from Customer c where age &amp;gt;= :age order by c.id&quot;)
                .parameterValues(parameters)
                .build();
    }

    @Bean
    public ItemProcessor&amp;lt;Customer, Customer2&amp;gt; customItemProcessor() {
        return new ItemProcessor&amp;lt;Customer, Customer2&amp;gt;() {
            @Override
            public Customer2 process(Customer item) throws Exception {
                return new Customer2(item);
            }
        };
    }


    @Bean
    public ItemWriter&amp;lt;Customer2&amp;gt; customItemWriter() {
        return new JpaItemWriterBuilder&amp;lt;Customer2&amp;gt;()
                .entityManagerFactory(entityManagerFactory)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세서를 이용해서 중간에 customer를 customer2로 만들어서 writer로 넘겨서 저장하는 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemWriterAdapter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 Job 안에 이미 있는 DAO나 다른 서비스를 ItemWriter 안에서 사용하고자 할 때 위임하는 기능을 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private final DataSource dataSource;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .build();
    }


    @Bean
    public ItemReader&amp;lt;Customer&amp;gt; customItemReader() {
        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;age&quot;, 25);

        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jpaPagingItemReader&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(10)
                .queryString(&quot;select c from Customer c where age &amp;gt;= :age order by c.id&quot;)
                .parameterValues(parameters)
                .build();
    }


    @Bean
    public ItemWriter&amp;lt;Customer&amp;gt; customItemWriter() {
        ItemWriterAdapter&amp;lt;Customer&amp;gt; writer = new ItemWriterAdapter&amp;lt;&amp;gt;();
        writer.setTargetObject(customService()); // 대상 클래스
        writer.setTargetMethod(&quot;customWrite&quot;); // 대상 메서드
        return writer;
    }

    @Bean
    public CustomService customService() {
        return new CustomService();
    }
}
----------------------------------------------------------
public class CustomService&amp;lt;T&amp;gt; {

    public void customWrite(T item){
        System.out.println(&quot;item = &quot; + item);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;writer에서 대상 클래스 메서드의 인자로 item들을 묶은 청크단위의 List가 넘어가는 게 아니라 하나씩의 item이 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같은 조건의 데이터를 읽고 수정할 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pay 엔티티의 상태가 false인 50개의 데이터를 배지 작업에서 상태를 true로 바꾸고 writer에서 update 하는 작업을 한다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Configuration
public class PayPagingFailJobConfiguration {

    public static final String JOB_NAME = &quot;payPagingFailJob&quot;;

    private final EntityManagerFactory entityManagerFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final JobBuilderFactory jobBuilderFactory;

    private final int chunkSize = 10;

    @Bean
    public Job payPagingJob() {
        return jobBuilderFactory.get(JOB_NAME)
                .start(payPagingStep())
                .build();
    }

    @Bean
    @JobScope
    public Step payPagingStep() {
        return stepBuilderFactory.get(&quot;payPagingStep&quot;)
                .&amp;lt;Pay, Pay&amp;gt;chunk(chunkSize)
                .reader(payPagingReader())
                .processor(payPagingProcessor())
                .writer(writer())
                .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader&amp;lt;Pay&amp;gt; payPagingReader() {
        return new JpaPagingItemReaderBuilder&amp;lt;Pay&amp;gt;()
                .queryString(&quot;SELECT p FROM Pay p WHERE p.successStatus = false&quot;)
                .pageSize(chunkSize)
                .entityManagerFactory(entityManagerFactory)
                .name(&quot;payPagingReader&quot;)
                .build();
    }

    @Bean
    @StepScope
    public ItemProcessor&amp;lt;Pay, Pay&amp;gt; payPagingProcessor() {
        return item -&amp;gt; {
            item.success();
            return item;
        };
    }

    @Bean
    @StepScope
    public JpaItemWriter&amp;lt;Pay&amp;gt; writer() {
        JpaItemWriter&amp;lt;Pay&amp;gt; writer = new JpaItemWriter&amp;lt;&amp;gt;();
        writer.setEntityManagerFactory(entityManagerFactory);
        return writer;
    }
}&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;배치 작업을 돌려보면 50개의 데이터가 배치작업이 되어있지 않을 것입니다. 10개씩 Chunk 단위로 작업을 진행하면 첫 번째 쿼리는 offset 0 limit 10 이지만 두번째 쿼리부터는 offset 11 limit 10이 나가게 되기 때문입니다. 첫번째 Chunk 작업에서 10개의 데이터가 success로 변경되었기 때문에 다시 쿼리가 나갈 때는 offset을 0부터 해야 합니다. 따라서 이때는 JpaPagingItemReader의 getPage 메서드를 override 해서 수정해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Bean
@StepScope
public JpaPagingItemReader&amp;lt;Pay&amp;gt; payPagingReader() {

    JpaPagingItemReader&amp;lt;Pay&amp;gt; reader = new JpaPagingItemReader&amp;lt;Pay&amp;gt;() {
        @Override
        public int getPage() {
            return 0;
        }
    };

    reader.setQueryString(&quot;SELECT p FROM Pay p WHERE p.successStatus = false&quot;);
    reader.setPageSize(chunkSize);
    reader.setEntityManagerFactory(entityManagerFactory);
    reader.setName(&quot;payPagingReader&quot;);

    return reader;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemWriter에 List 전달하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Processor에서 List형태로 반환하게 되면 Writer는 이걸 바로 해결할 수 없습니다. 만약 청크 사이즈가 10이라면 10개의 리스트를 하나의 리스트 안에 담아서 itemWriter에 넘기게 됩니다. 따라서 itemWriter의 Write 메서드를 재정의하여 하나의 리스트로 풀어주고 해당 데이터를 write 하는 과정으로 수정해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class JpaItemListWriter&amp;lt;T&amp;gt; extends JpaItemWriter&amp;lt;List&amp;lt;T&amp;gt;&amp;gt; {

    private JpaItemWriter&amp;lt;T&amp;gt; jpaItemWriter;

    public JpaItemListWriter(JpaItemWriter&amp;lt;T&amp;gt; jpaItemWriter) {
        this.jpaItemWriter = jpaItemWriter;
    }

    @Override
    public void write(List&amp;lt;? extends List&amp;lt;T&amp;gt;&amp;gt; items) {
        List&amp;lt;T&amp;gt; totalList = new ArrayList&amp;lt;&amp;gt;();

        for (List&amp;lt;T&amp;gt; list: items) {
            totalList.addAll(list);
        }

        jpaItemWriter.write(totalList);
    }
}&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;write를 재정의해서 전달받은 데이터의 리스트를 풀어서 하나의 리스트에 담고 기존 jpaItemWriter에게 쓰기 작업을 다시 시킨 코드입니다. Job에 적용할 때는 아래와 같이 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class SimpleJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .incrementer(new RunIdIncrementer())
                .start(step())
                .build();
    }

    public Step step() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, List&amp;lt;Customer2&amp;gt;&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .processor(customItemProcessor())
                .writer(customItemWriter())
                .build();
    }

    @Bean
    public JpaPagingItemReader&amp;lt;Customer&amp;gt; customItemReader() {
        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;customItemReader&quot;)
                .pageSize(chunkSize)
                .entityManagerFactory(entityManagerFactory)
                .queryString(&quot;select c from Customer c order by c.id&quot;)
                .build();

    }
    @Bean
    public ItemProcessor&amp;lt;Customer, List&amp;lt;Customer2&amp;gt;&amp;gt; customItemProcessor() {
        return item -&amp;gt; Arrays.asList(new Customer2(item.getName(), item.getAge()));
    }

    @Bean
    public JpaItemListWriter&amp;lt;Customer2&amp;gt; customItemWriter() {
        JpaItemWriter&amp;lt;Customer2&amp;gt; writer = new JpaItemWriter&amp;lt;&amp;gt;();
        writer.setEntityManagerFactory(entityManagerFactory);

        JpaItemListWriter&amp;lt;Customer2&amp;gt; jpaItemListWriter = new JpaItemListWriter&amp;lt;&amp;gt;(writer);
        jpaItemListWriter.setEntityManagerFactory(entityManagerFactory);
        return jpaItemListWriter;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Spring</category>
      <category>ItemWriter</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/67</guid>
      <comments>https://backtony.tistory.com/67#entry67comment</comments>
      <pubDate>Sun, 28 Jan 2024 16:12:33 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - ItemReader</title>
      <link>https://backtony.tistory.com/66</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemReader&lt;/h2&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;855&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCQBiJ/btsD3I7UyNv/d61SroneXFVce4FJOkgKM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCQBiJ/btsD3I7UyNv/d61SroneXFVce4FJOkgKM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCQBiJ/btsD3I7UyNv/d61SroneXFVce4FJOkgKM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCQBiJ%2FbtsD3I7UyNv%2Fd61SroneXFVce4FJOkgKM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;855&quot; height=&quot;181&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;181&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;itemReader는 다양한 입력으로부터 데이터를 읽어서 제공하는 인터페이스입니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플랫 파일 - csv, txt&lt;/li&gt;
&lt;li&gt;XML, Json&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;Message Queuing 서비스&lt;/li&gt;
&lt;li&gt;Custom reader&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다수의 구현체들이 ItemReader와 ItemStream 인터페이스를 동시에 구현하고 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemStream은 파일 스트림 연결 종료, DB 커넥션 연결 종료 등의 장치 초기화 등의 작업에 사용됩니다.&lt;/li&gt;
&lt;li&gt;ExecutionContext에 read와 관련된 여러 가지 상태 정보를 저장해 두고 재시작 시 참조됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet 실행 시 필수적 요소로 설정해야 합니다.&lt;/li&gt;
&lt;li&gt;T read()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 데이터를 읽고 다음 데이터로 이동합니다.&lt;/li&gt;
&lt;li&gt;아이템 하나를 리턴하며 더 이상 아이템이 없는 경우 null 리턴합니다.&lt;/li&gt;
&lt;li&gt;아이템 하나는 파일의 한 줄, DB의 한 row, XML 파일에서 하나의 엘리먼트를 의미합니다.&lt;/li&gt;
&lt;li&gt;더 이상 처리해야 할 item이 없어도 예외가 발생하지 않고 itemProcessor와 같은 다음 단계로 넘어갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;I,O&amp;gt;Chunk
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;I : Reader가 읽는 데이터 타입&lt;/li&gt;
&lt;li&gt;O : Writer가 받은 데이터 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/593Ro/btsD2u3EZbq/BfB79U2BDXCESlVNGuoAC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/593Ro/btsD2u3EZbq/BfB79U2BDXCESlVNGuoAC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/593Ro/btsD2u3EZbq/BfB79U2BDXCESlVNGuoAC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F593Ro%2FbtsD2u3EZbq%2FBfB79U2BDXCESlVNGuoAC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1624&quot; height=&quot;624&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 다양한 구현체들을 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cursor 기반 &amp;amp; Paging 기반 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 애플리케이션은 실시간적 처리가 어려운 대용량 데이터를 다루며 이때 DB I/O의 성능 문제와 메모리 자원의 효율성 문제를 해결할 수 있어야 합니다. 스프링 배치에서는 대용량 데이터 처리를 위한 두 가지 해결방안을 제시하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cursor 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC ResultSet의 기본 메커니즘을 사용합니다. &lt;b&gt;현재 행에서 커서를 유지하며 다음 데이터를 호출하면 다음 행으로 커서를 이동하며 데이터 반환이 이뤄지는 Streaming 방식의 I/O 입니다.&lt;/b&gt; ResultSet이 open 될 때마다 next 메서드가 호출되어 DataBase의 데이터 하나가 반환되고 객체와 매핑이 이뤄집니다. DB Connection이 연결되면 배치 처리가 완료될 때까지 Connection이 유지되기 때문에 DB와 SocketTimeout을 충분히 큰 값으로 설정해야 합니다. &lt;b&gt;모든 결과를 메모리에 할당하기 때문에 메모리 사용량이 많아지는 단점이 있습니다.&lt;/b&gt; Connection 연결 유지 시간과 메모리 공간이 충분하다면 대량의 데이터 처리에 적합할 수 있습니다.(fetchSize 조절로 한 번에 가져올 수도 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Paging 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페이징 단위로 데이터를 조회하는 방식으로 Page Size만큼 한 번에 메모리로 가져온 다음 한 개씩 데이터를 읽는 방식입니다.&lt;/b&gt; 한 페이지를 읽을 때마다 커넥션을 맺고 끊기를 반복하기 때문에 대량의 데이터를 처리하더라도 SocketTimeout 예외가 거의 발생하지 않습니다. 시작 행 번호를 지정하고 페이지에 반환시키고자 하는 행의 수를 지정한 후 사용합니다.(offest, limit) 페이징 단위의 결과만 메모리에 할당하기 때문에 메모리 사용량이 적어지는 장점이 있습니다. 따라서 커넥션 유지 시간이 길지 않고 메모리를 효율적으로 사용해야 하는 데이터 처리에 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비교&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1577&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkulGg/btsD27UFm9C/3WD7wPkvn3klie7HemLmFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkulGg/btsD27UFm9C/3WD7wPkvn3klie7HemLmFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkulGg/btsD27UFm9C/3WD7wPkvn3klie7HemLmFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkulGg%2FbtsD27UFm9C%2F3WD7wPkvn3klie7HemLmFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1577&quot; height=&quot;743&quot; data-origin-width=&quot;1577&quot; data-origin-height=&quot;743&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cursor 기반
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 데이터를 하나씩 처리(fetchSize를 이용해서 한 번에 처리가 가능하긴 함)&lt;/li&gt;
&lt;li&gt;모든 데이터를 처리할 때까지 커넥션 유지&lt;/li&gt;
&lt;li&gt;모든 결과를 메모리에 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Paging 기반
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Page Size만큼의 데이터를 한번에 처리&lt;/li&gt;
&lt;li&gt;Page Size만큼의 처리를 할 때마다 커넥션을 맺고 끊음&lt;/li&gt;
&lt;li&gt;페이징 단위 결과만 메모리에 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JdbcCursorItemReader&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cursor 기반의 JDBC 구현체로서&lt;/b&gt; ResultSet과 함께 사용되며 Datasource에서 connection을 얻어와서 SQL을 실행합니다. Thread 안정성을 보장하지 않기 때문에 멀티 스레드 환경에서 사용할 경우 동시성 이슈가 발생하지 않도록 &lt;b&gt;동기화 처리를 별도로 진행해야&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1608&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkMQkt/btsD2tDG82J/X9kEaRk8TbF1z1veOm5y5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkMQkt/btsD2tDG82J/X9kEaRk8TbF1z1veOm5y5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkMQkt/btsD2tDG82J/X9kEaRk8TbF1z1veOm5y5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkMQkt%2FbtsD2tDG82J%2FX9kEaRk8TbF1z1veOm5y5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1608&quot; height=&quot;760&quot; data-origin-width=&quot;1608&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커서를 오픈하면서 DB 커넥션 연결, PrepareStatement 생성, ResultSet 생성 등 준비 작업을 합니다.&lt;/li&gt;
&lt;li&gt;JdbcCursorItemReader에서 데이터를 한건씩 가져오는 작업을 Chunk Size만큼 반복합니다.&lt;/li&gt;
&lt;li&gt;모든 배치 작업이 완료되고 더 이상 읽을 데이터가 없어지면 커넥션 종료 등 리소스를 해제하고 작업을 종료합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&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;1442&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcUS3g/btsD4swODZK/SuCkkKsAuhhBY74vYSzpTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcUS3g/btsD4swODZK/SuCkkKsAuhhBY74vYSzpTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcUS3g/btsD4swODZK/SuCkkKsAuhhBY74vYSzpTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcUS3g%2FbtsD4swODZK%2FSuCkkKsAuhhBY74vYSzpTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1442&quot; height=&quot;620&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;Customer&amp;gt; customItemReader() {
        return new JdbcCursorItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jdbcCursorItemReader&quot;)
                .fetchSize(chunkSize)
                .sql(&quot;select id, name, age from customer where age &amp;gt;= ?&quot;)
                .beanRowMapper(Customer.class)
                .queryArguments(25)
                .dataSource(dataSource)
                .build();
    }

    @Bean
    public ItemWriter&amp;lt;Customer&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            for (Customer item : items) {
                System.out.println(&quot;item = &quot; + item.toString());
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 Customer 객체를 100개 넣어두고 조건에 맞게 가져와서 출력하는 간단한 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JpaCursorItemReader&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cursor 기반의 JPA 구현체로서&lt;/b&gt; EntityManagerFactory 객체가 필요하여 쿼리는 JPQL을 사용합니다. Spring Batch 4.3 버전부터 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리 과정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLRCJl/btsD7nn9FI4/uptgLGkGRE9OXZ1vRubgK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLRCJl/btsD7nn9FI4/uptgLGkGRE9OXZ1vRubgK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLRCJl/btsD7nn9FI4/uptgLGkGRE9OXZ1vRubgK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLRCJl%2FbtsD7nn9FI4%2FuptgLGkGRE9OXZ1vRubgK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1466&quot; height=&quot;752&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;752&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;open 하는 과정에서 엔티티 매니저 생성, 작성한 JPQL을 실행시켜서 DB에서 값을 받아와서 ResultStream에 데이터를 담아놓습니다. 실상 open 작업에서 데이터를 가져오는 작업이 다 끝납니다.&lt;/li&gt;
&lt;li&gt;JpaCursorItemReader는 ResultStream에서 이터레이터로 하나씩 데이터를 가져옵니다.&lt;/li&gt;
&lt;li&gt;close에서는 EntityManager만 닫습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wtX7j/btsD4lYW2AB/n6CQhdhzAjFeyCYHjFxlXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wtX7j/btsD4lYW2AB/n6CQhdhzAjFeyCYHjFxlXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wtX7j/btsD4lYW2AB/n6CQhdhzAjFeyCYHjFxlXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwtX7j%2FbtsD4lYW2AB%2Fn6CQhdhzAjFeyCYHjFxlXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1262&quot; height=&quot;560&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;Customer&amp;gt; customItemReader() {

        HashMap&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;age&quot;,25);

        return new JpaCursorItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jpaCursorItemReader&quot;)
                .entityManagerFactory(entityManagerFactory)
                .queryString(&quot;select c from Customer c where age &amp;gt;=:age &quot;) // JPQL -&amp;gt; 테이블명은 첫글자만 대문자
                .parameterValues(parameters)
                .build();
    }

    @Bean
    public ItemWriter&amp;lt;Customer&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            for (Customer item : items) {
                System.out.println(&quot;item = &quot; + item.toString());
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JdbcPagingItemReader&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Paging 기반의 JDBC 구현체로 쿼리에 시작 행 번호(offset)와 페이지에서 변환할 행수(limit)를 지정해서 SQL을 실행합니다. 스프링 배치에서 offset과 limit을 PageSize에 맞게 자동으로 생성해 주며 페이징 단위로 데이터를 조회할 때마다 새로운 쿼리가 실행됩니다. 페이지마다 새로운 쿼리를 실행하기 때문에 페이징 시 결과 데이터의 순서가 보장될 수 있도록 &lt;b&gt;order by 구문이 필수입니다.&lt;/b&gt; &lt;b&gt;멀티 스레드 환경에서 Thread 안정성을 보장하기&lt;/b&gt; 때문에 별도의 동기화 처리가 필요하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PagingQueryProvider&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 실행에 필요한 쿼리문을 ItemReader에게 제공하는 클래스입니다. 데이터베이스마다 페이징 전략이 다르기 때문에 각 데이터 베이스 유형마다 다른 PaingQueryProvider을 사용하게 되는데 이는 DataSource 설정 값을 보고 자동으로 선택합니다. Select, from, sortKey는 필수로 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1597&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjj978/btsD2wmTlhS/JCkisFrsNGKgTPf3MYTAJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjj978/btsD2wmTlhS/JCkisFrsNGKgTPf3MYTAJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjj978/btsD2wmTlhS/JCkisFrsNGKgTPf3MYTAJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjj978%2FbtsD2wmTlhS%2FJCkisFrsNGKgTPf3MYTAJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1597&quot; height=&quot;664&quot; data-origin-width=&quot;1597&quot; data-origin-height=&quot;664&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;open에서 update로 ExecutionContext에 상태정보를 업데이트합니다.&lt;/li&gt;
&lt;li&gt;JdbcPagingItemReader에서 JdbcTemplate을 이용해 쿼리를 날리고 페이지 사이즈만큼 데이터를 가져옵니다.(커넥션 얻고 종료)&lt;/li&gt;
&lt;li&gt;이 과정을 청크 사이즈만큼 반복합니다.(보통 청크 사이즈와 페이징 사이즈를 일치시키는 것을 권장합니다.)&lt;/li&gt;
&lt;li&gt;더 이상 처리할 것이 없으면 종료하게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&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;1464&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UEQ8T/btsD3q7nQlL/kwr8RAQF9a2tgJW2y0xQKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UEQ8T/btsD3q7nQlL/kwr8RAQF9a2tgJW2y0xQKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UEQ8T/btsD3q7nQlL/kwr8RAQF9a2tgJW2y0xQKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUEQ8T%2FbtsD3q7nQlL%2Fkwr8RAQF9a2tgJW2y0xQKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1464&quot; height=&quot;792&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;792&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;queryProvider가 제공된다면 위의 빨간 박스 내용은 작성하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DataSource dataSource;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() throws Exception {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() throws Exception {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;Customer&amp;gt; customItemReader() throws Exception {

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;age&quot;,25);

        return new JdbcPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jdbcPagingItemReader&quot;)
                .pageSize(10)
                .dataSource(dataSource)
                .rowMapper(new BeanPropertyRowMapper&amp;lt;&amp;gt;(Customer.class))
                .queryProvider(createQueryProvider()) // 쿼리 생성
                .parameterValues(parameters) // 파라미터 입력
                .build();
    }

    @Bean
    public PagingQueryProvider createQueryProvider() throws Exception {
        Map&amp;lt;String, Order&amp;gt; sortKeys = new HashMap&amp;lt;&amp;gt;();
        sortKeys.put(&quot;id&quot;, Order.ASCENDING);

        // 쿼리 생성
        SqlPagingQueryProviderFactoryBean queryProvider = new SqlPagingQueryProviderFactoryBean();
        queryProvider.setDataSource(dataSource);
        queryProvider.setSelectClause(&quot;id,name,age&quot;); // select 절
        queryProvider.setFromClause(&quot;from customer&quot;); // from 절
        queryProvider.setWhereClause(&quot;where age &amp;gt; :age&quot;); // where 절
        queryProvider.setSortKeys(sortKeys); // order by 절

        return queryProvider.getObject();
    }

    @Bean
    public ItemWriter&amp;lt;Customer&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            for (Customer item : items) {
                System.out.println(&quot;item = &quot; + item.toString());
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JpaPagingItemReader&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Paging 기반의 JPA 구현체로&lt;/b&gt; EntityManagerFactory 객체가 필요하며 쿼리는 JPQL을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1451&quot; data-origin-height=&quot;665&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HfGOD/btsD1bwvGRu/YAxdpNynIKOcM8rcDgsfVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HfGOD/btsD1bwvGRu/YAxdpNynIKOcM8rcDgsfVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HfGOD/btsD1bwvGRu/YAxdpNynIKOcM8rcDgsfVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHfGOD%2FbtsD1bwvGRu%2FYAxdpNynIKOcM8rcDgsfVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1451&quot; height=&quot;665&quot; data-origin-width=&quot;1451&quot; data-origin-height=&quot;665&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;open에서 엔티티 매니저를 생성합니다.&lt;/li&gt;
&lt;li&gt;JpaPagingItemReader에서 엔티티 매니저로를 사용해 쿼리를 날려 데이터를 가져옵니다.(커넥션 얻고 종료)&lt;/li&gt;
&lt;li&gt;이 과정을 청크 사이즈만큼 반복합니다.&lt;/li&gt;
&lt;li&gt;더 이상 읽을 데이터가 없으면 엔티티 매니저를 종료합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API&lt;/h3&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;1272&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9TvKO/btsD1b4lAA6/qfzRhLBIO8BRnOFvRVSR40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9TvKO/btsD1b4lAA6/qfzRhLBIO8BRnOFvRVSR40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9TvKO/btsD1b4lAA6/qfzRhLBIO8BRnOFvRVSR40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9TvKO%2FbtsD1b4lAA6%2FqfzRhLBIO8BRnOFvRVSR40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;517&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;Customer, Customer&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;Customer&amp;gt; customItemReader() {

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;age&quot;,25);

        return new JpaPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jpaPagingItemReader&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(10)
                .queryString(&quot;select c from Customer c where age &amp;gt;= :age order by c.id&quot;)
                .parameterValues(parameters)
                .build();
    }


    @Bean
    public ItemWriter&amp;lt;Customer&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            for (Customer item : items) {
                System.out.println(&quot;item = &quot; + item.toString());
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemReaderAdapter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재하는 DAO나 다른 서비스를 ItemReader 안에서 사용하고자 할 때 위임하는 역할을 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class HelloJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private int chunkSize = 10;

    @Bean
    public Job helloJob() {
        return jobBuilderFactory.get(&quot;job&quot;)
                .start(step1())
                .incrementer(new RunIdIncrementer())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step&quot;)
                .&amp;lt;String, String&amp;gt;chunk(chunkSize)
                .reader(customItemReader())
                .writer(customItemWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; customItemReader() {

        ItemReaderAdapter&amp;lt;String&amp;gt; reader = new ItemReaderAdapter&amp;lt;&amp;gt;();
        reader.setTargetObject(customerService()); // 클래스 이름
        reader.setTargetObject(&quot;customRead&quot;); // 메서드 명
        return reader;
    }


    @Bean
    public ItemWriter&amp;lt;String&amp;gt; customItemWriter() {
        return items -&amp;gt; {
            for (String item : items) {
                System.out.println(&quot;item = &quot; + item.toString());
            }
        };
    }
}&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;ItemReaderAdapter를 이용해서 클래스의 이름과 메서드명을 명시하면 해당 메서드가 Reader의 역할을 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Page Size와 Chunk Size를 일치해야 하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk Size가 50이고 Page Size가 10이라고 가정했을 때, Chunk Size를 채우기 위해 5번의 Read가 발생합니다. 5번의 Read가 발생한 뒤에 itemProcessor로 넘기게 되는데 itemProcessor에서 만약 item의 LazyLoading이 발생한다면 이때 문제가 생깁니다. &lt;b&gt;이유는 5번의 Read가 발생하는 동안 각각 트랜잭션이 초기화되기 때문입니다.&lt;/b&gt; &lt;b&gt;이 문제는 Page Size와 Chunk Size를 일치시키면 해결할 수 있습니다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>itemReader</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/66</guid>
      <comments>https://backtony.tistory.com/66#entry66comment</comments>
      <pubDate>Sun, 28 Jan 2024 15:57:00 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - 청크 기반 프로세스</title>
      <link>https://backtony.tistory.com/65</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Chunk&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;여러 개의 아이템을 묶은 하나의 덩어리를&lt;/b&gt; 의미합니다.&lt;/li&gt;
&lt;li&gt;한 번에 하나씩 아이템을 입력받아 Chunk 단위의 덩어리로 만든 후 Chunk 단위로 트랜잭션을 처리합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk 단위로 Commit과 Rollback&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Chunk &amp;lt;I&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chunk&amp;lt;I&amp;gt;, Chunk &amp;lt;O&amp;gt;&lt;/h3&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;Chunk &amp;lt;I&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemReader로 읽은 하나의 아이템을 Chunk 크기만큼 반복해서 저장하는 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Chunk &amp;lt;O&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemReader로부터 전달받은 Chunk &amp;lt;I&amp;gt;를 참조해서 ItemProcessor에서 적절하게 가공한 뒤 ItemWriter로 전달되는 타입&lt;/li&gt;
&lt;li&gt;여기서 O는 Processor가 없다면 ItemReader로부터 전달받는 타입, Processor가 있다면 Processor로부터 전달받는 타입입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍처&lt;/h3&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;1612&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzIKYU/btsDXCy3Vha/BW0O1cSBD3aeHrZtKGEAi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzIKYU/btsDXCy3Vha/BW0O1cSBD3aeHrZtKGEAi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzIKYU/btsDXCy3Vha/BW0O1cSBD3aeHrZtKGEAi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzIKYU%2FbtsDXCy3Vha%2FBW0O1cSBD3aeHrZtKGEAi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1612&quot; height=&quot;538&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;538&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ItemReader가 Source를 한 건씩 읽고 한 건씩 Chunk크기만큼 Chunk &amp;lt;I&amp;gt;에 저장합니다.&lt;/li&gt;
&lt;li&gt;Chunk 크기만큼 쌓였다면 Chunk &amp;lt;I&amp;gt;를 ItemProcessor에 전달합니다.&lt;/li&gt;
&lt;li&gt;ItemProcessor는 전달받은 Chunk를 적절하게 가공해서 Chunk &amp;lt;O&amp;gt;에 저장합니다.&lt;/li&gt;
&lt;li&gt;Chunk &amp;lt;O&amp;gt;를 ItemWriter에 전달합니다.&lt;/li&gt;
&lt;li&gt;itemWriter는 데이터를 쓰기 작업합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ItemReader와 ItemProcessor는 각각의 하나씩 아이템을 처리하지면 ItemWriter는 Chunk 크기만큼을 한 번에 일괄 처리합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ChunkOrientedTasklet&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 배치에서 제공하는 &lt;b&gt;Tasklet 구현체로 Chunk 지향 프로세싱을 담당하는 도메인 객체입니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ItemReader, ItemWriter, ItemProcessor를 사용해 Chunk 기반 데이터 입출력을 담당합니다.&lt;/li&gt;
&lt;li&gt;TaskletStep에 의해서 반복적으로 실행되며, ChunkOrientedTasklet이 실행될 때마다 &lt;b&gt;매번 새로운 트랜잭션이 생성되어&lt;/b&gt; 처리됩니다.&lt;/li&gt;
&lt;li&gt;exception이 발생할 경우, 해당 Chunk는 롤백되며 이전에 커밋한 Chunk는 완료 상태가 유지됩니다.&lt;/li&gt;
&lt;li&gt;내부적으로 ItemReader를 핸들링하는 ChunkProvider와 ItemProcessor, ItemWriter를 핸들링하는 ChunkProcessor 타입의 구현체를 갖습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ChunkProvider&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ItemReader를 사용해서 소스로부터 아이템을 Chunk size만큼 읽어서 Chunk단위로 만들어 제공하는 도메인 객체&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Chunk &amp;lt;I&amp;gt;를 만들고 내부적으로 반복문을 사용해서 ItemReader.read()를 계속 호출하면서 item을 Chunk &amp;lt;I&amp;gt;에 쌓습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk size만큼 item을 읽으면 반복문이 종료되고 ChunkProcessor로 넘깁니다.&lt;/li&gt;
&lt;li&gt;ItemReader가 읽은 item이 null일 경우 read 반복문이 종료되고 해당 Step의 반복문도 종료됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;외부로부터 ChunkProvider이 호출될 때마다 &lt;b&gt;새로운 Chunk를&lt;/b&gt; 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ChunkProcessor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ItemProcessor를 사용해서 Item을 변형, 가공, 필터링하고 ItemWriter를 사용해서 Chunk 데이터를 저장, 출력합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Chunk &amp;lt;O&amp;gt;를 만들고 앞에서 넘어온 Chunk &amp;lt;I&amp;gt;의 item을 &lt;b&gt;한 건씩&lt;/b&gt; itemProcessor를 통해 처리한 후 Chunk &amp;lt;O&amp;gt;에 저장합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 ItemProcessor가 존재하지 않는다면 바로 Chunk&amp;lt;O&amp;gt;에 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ItemProcessor 처리가 완료되면 Chunk&amp;lt;O&amp;gt;에 있는 List &amp;lt;item&amp;gt;을 ItemWriter에게 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ItemWriter 처리가 완료되면 Chunk 트랜잭션이 종료되고 Step 반복문에서는 다시 ChunkOrientedTasklet가 새롭게 실행됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ItemWriter는 Chunk size만큼 데이터를 커밋하기 때문에 Chunk size는 곧 Commit Interval(커밋 간격)이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&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;1624&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sAqZ8/btsDT5vYNYU/k8ZW3KV9pFChRIiwkSZ5S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sAqZ8/btsDT5vYNYU/k8ZW3KV9pFChRIiwkSZ5S1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sAqZ8/btsDT5vYNYU/k8ZW3KV9pFChRIiwkSZ5S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsAqZ8%2FbtsDT5vYNYU%2Fk8ZW3KV9pFChRIiwkSZ5S1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1624&quot; height=&quot;754&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;754&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TaskletStep이 execute 메서드로 ChunkOrientedTasklet를 호출합니다.&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet는 provide 메서드로 ChunkProvider를 호출합니다.&lt;/li&gt;
&lt;li&gt;ChunkProvider는 ItemReader에게 Item을 &lt;b&gt;한 건씩&lt;/b&gt; read 하도록 지시합니다.&lt;/li&gt;
&lt;li&gt;이 과정이 Chunk size만큼 반복됩니다.&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet는 ChunkProcessor에게 읽은 데이터를 가공하라고 명령합니다.&lt;/li&gt;
&lt;li&gt;ChunkProcessor는 ItemProcessor에게 명령하고 ItemProcessor는 전달된 아이템 개수만큼 반복하여 가공합니다.&lt;/li&gt;
&lt;li&gt;ChunkProcessor는 가공된 아이템을 ItemWriter에 전달합니다.&lt;/li&gt;
&lt;li&gt;ItemWriter는 저장하는 등 쓰기 처리를 합니다.&lt;/li&gt;
&lt;li&gt;이것이 하나의 Chunk Size 사이클로 이후 다시 ChunkOrientedTasklet에 가서 읽을 Item이 없을 때까지 반복합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ItemReader&lt;/h3&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;855&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FdbyT/btsDTvIeeNV/nRAYCP1sXIrFIn8kkpW5V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FdbyT/btsDTvIeeNV/nRAYCP1sXIrFIn8kkpW5V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FdbyT/btsDTvIeeNV/nRAYCP1sXIrFIn8kkpW5V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFdbyT%2FbtsDTvIeeNV%2FnRAYCP1sXIrFIn8kkpW5V1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;855&quot; height=&quot;181&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;181&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;다양한 입력으로부터 데이터를 읽어서 제공하는 인터페이스입니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플랫 파일 - csv, txt&lt;/li&gt;
&lt;li&gt;XML, Jsono&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;Message Queuing 서비스&lt;/li&gt;
&lt;li&gt;Custom reader&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다수의 구현체들이 ItemReader와 ItemStream 인터페이스를 동시에 구현하고 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemStream은 파일 스트림 연결 종료, DB 커넥션 연결 종료 등의 장치 초기화 등의 작업에 사용됩니다.&lt;/li&gt;
&lt;li&gt;ExecutionContext에 read와 관련된 여러 가지 상태 정보를 저장해 두고 재시작 시 참조됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet 실행 시 필수적 요소로 설정해야 합니다.&lt;/li&gt;
&lt;li&gt;T read()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 데이터를 읽고 다음 데이터로 이동합니다.&lt;/li&gt;
&lt;li&gt;아이템 하나를 리턴하며 더 이상 아이템이 없는 경우 null 리턴합니다.&lt;/li&gt;
&lt;li&gt;아이템 하나는 파일의 한 줄, DB의 한 row, XML 파일에서 하나의 엘리먼트를 의미합니다.&lt;/li&gt;
&lt;li&gt;더 이상 처리해야 할 item이 없어도 예외가 발생하지 않고 itemProcessor와 같은 다음 단계로 넘어갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ItemWriter&lt;/h3&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;710&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YbKhr/btsDSq1tk9q/iwI4BluVbVT60EcrKUow1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YbKhr/btsDSq1tk9q/iwI4BluVbVT60EcrKUow1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YbKhr/btsDSq1tk9q/iwI4BluVbVT60EcrKUow1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYbKhr%2FbtsDSq1tk9q%2FiwI4BluVbVT60EcrKUow1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;160&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;160&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Chunk 단위로 데이터를 받아 일괄 출력 작업을 위한 인터페이스입니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플랫 파일 - csv, txt&lt;/li&gt;
&lt;li&gt;XML, Jsono&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;Message Queuing 서비스&lt;/li&gt;
&lt;li&gt;Mail Service&lt;/li&gt;
&lt;li&gt;Custom reader&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다수의 구현체들이 itemReader와 같은 맥락으로 itemWriter와 ItemStream을 동시에 구현하고 있습니다.&lt;/li&gt;
&lt;li&gt;하나의 아이템이 아닌 아이템 리스트를 전달받아 수행합니다.&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet 실행 시 필수적 요소로 설정해야 합니다.&lt;/li&gt;
&lt;li&gt;void write()
&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;출력이 완료되고 트랜잭션이 종료되면 새로운 Chunk 단위 프로세스로 이동합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ItemProcessor&lt;/h3&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;733&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8C5dT/btsDQ9Tdued/pwCPyo2RKc9ulsP8A1gemK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8C5dT/btsDQ9Tdued/pwCPyo2RKc9ulsP8A1gemK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8C5dT/btsDQ9Tdued/pwCPyo2RKc9ulsP8A1gemK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8C5dT%2FbtsDQ9Tdued%2FpwCPyo2RKc9ulsP8A1gemK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;733&quot; height=&quot;182&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;182&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터를 출력하기 전에 데이터를 가공 및 필터링 역할을 하는 인터페이스입니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ItemReader 및 ItemWriter와 분리되어 비즈니스 로직을 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;ItemReader로부터 받은 아이템을 특정 타입으로 변환해서 ItemWriter에 넘겨줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;Itemreader로부터 받은 아이템들 중 필터과정을 거쳐서 원하는 아이템들만 ItemWriter로 넘겨줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet 실행 시 선택적 요소기 때문에 필수 요소는 아닙니다.&lt;/li&gt;
&lt;li&gt;O process()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;I 제네릭은 ItemReader에서 받을 데이터 타입&lt;/li&gt;
&lt;li&gt;O 제네릭은 ItemWriter에게 보낼 데이터 타입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아이템을 하나씩 가공 처리하며 null을 리턴할 경우 해당 아이템은 Chunk &amp;lt;O&amp;gt;에 저장되지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ItemStream을 구현하지 않고 거의 대부분 Customizing 해서 사용하기 때문에 기본적으로 제공되는 구현체가 적습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ItemStream&lt;/h3&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;987&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvQ3LK/btsDQ1nkpDu/SJze3ewWEBzOEk2hc3dTC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvQ3LK/btsDQ1nkpDu/SJze3ewWEBzOEk2hc3dTC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvQ3LK/btsDQ1nkpDu/SJze3ewWEBzOEk2hc3dTC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvQ3LK%2FbtsDQ1nkpDu%2FSJze3ewWEBzOEk2hc3dTC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;987&quot; height=&quot;402&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;402&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemReader와 ItemWriter 처리 과정 중 상태를 저장하고 오류가 발생하여 재시작 시 해당 상태를 참조하여 실패한 곳부터 재시작하도록 지원합니다.&lt;/li&gt;
&lt;li&gt;리소스를 열고(open) 닫아야(close) 하며 입출력 장치 초기화 등의 작업을 해야 하는 경우 사용합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 구현체는 다 만들어져 있기 때문에 구현체를 사용한다면 직접 구현할 일은 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;open과 update 메서드에서 ExecutionContext를 인수로 받는데 이는 상태 정보를 ExecutionContext에 업데이트해두고 재시작 시 open에서 해당 정보를 가져와서 사용하기 때문입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, 총 10개의 데이터를 Chunk 5 단위로 진행한다면 총 2번의 update가 발생합니다. 만약 9번째 데이터를 읽는 과정에서 문제가 발생하면 첫 번째 청크 커밋은 완료가 된 상태로 재시작 시에는 open에서 ExecutionContext에서 정보를 가져와 6번째 데이터부터 다시 시작할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stream이 구현된 ItemReader와 ItemWriter를 직접 만들려면 ItemStreamReader, ItemStreamWrtier 인터페이스를 구현하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;청크 기반 프로세스 아키텍처&lt;/h2&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;1635&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br44MH/btsDUaX4diC/X1FlPZdEsGyCOUyC5HnnXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br44MH/btsDUaX4diC/X1FlPZdEsGyCOUyC5HnnXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br44MH/btsDUaX4diC/X1FlPZdEsGyCOUyC5HnnXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr44MH%2FbtsDUaX4diC%2FX1FlPZdEsGyCOUyC5HnnXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1635&quot; height=&quot;837&quot; data-origin-width=&quot;1635&quot; data-origin-height=&quot;837&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Job을 실행하면 TaskletStep이 실행됩니다.&lt;/li&gt;
&lt;li&gt;Tasklet은 내부에 RepeatTemplate라는 반복기를 가지고 있어 ChunkOrientedTasklet을 반복합니다.&lt;/li&gt;
&lt;li&gt;ChunkOrientedTasklet이 실행될 때 스프링 배치는 Transaction 경계를 생성합니다.&lt;/li&gt;
&lt;li&gt;Chunk 단위로 작업을 시작합니다.&lt;/li&gt;
&lt;li&gt;SimpleChunkProvider도 내부적으로 RepeatTmplate 반복기를 갖고 있어 ItemReader을 Chunk size만큼 반복시켜서 데이터를 읽습니다.&lt;/li&gt;
&lt;li&gt;Chunk Size만큼 읽고 읽은 아이템이 담긴 Chunk&amp;lt;I&amp;gt;를 SimpleChunkProcessor에 넘깁니다.&lt;/li&gt;
&lt;li&gt;SimpleChunkProcessor는 전달받은 Chunk 데이터를 한 건씩 읽어서 ItemProcessor로 데이터를 가공하여 Chunk&amp;lt;O&amp;gt;에 저장합니다.&lt;/li&gt;
&lt;li&gt;ItemWriter에게 Chunk가 갖고 있는 List값을 전달하고 ItemWriter는 출력 처리를 합니다.(트랜잭션 커밋)&lt;/li&gt;
&lt;li&gt;이 과정이 청크 단위로 반복되고 ItemReader에서 null 값을 읽을 때 반복 작업이 끝나게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 예외가 발생한다면 트랜잭션 롤백이 발생하고 작업이 중단되며, ItemReader에서 null값을 읽어오게 된다면 RepeatStatus.FINISHED를 통해 현재 작업을 마지막으로 다음부터는 반복 작업이 일어나지 않게 됩니다. &lt;b&gt;Chunk 단위마다 새로운 트랜잭션&lt;/b&gt; 이 생성되고 커밋되는 과정이 존재하는 것을 알아둡시다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/65</guid>
      <comments>https://backtony.tistory.com/65#entry65comment</comments>
      <pubDate>Thu, 25 Jan 2024 00:12:07 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch - DB 스키마와 도메인</title>
      <link>https://backtony.tistory.com/64</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;배치 애플리케이션이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치(Batch)는 일괄처리란 뜻을 갖고 있습니다.&lt;/b&gt; 쇼핑몰에서 매일 전날의 매출 데이터를 집계해야 한다고 가정해 보겠습니다. 매출 데이터가 대용량이라면 하루 매출 데이터를 읽고, 가공하고, 저장한다면 해당 서버는 순식간에 CPU, I/O 등의 자원을 다 써버려서 다른 작업을 할 수 없게 됩니다. 집계 기능은 하루에 1번만 수행된다면 이를 위해 API를 구성하는 것은 낭비가 될 수 있고, 데이터 처리 중에 실패했다면 처음부터가 아니라 실패시점부터 다시 처리하고 싶을 수 있습니다. 이런 단발성으로 &lt;b&gt;대용량의 데이터를 처리하는 애플리케이션을 배치 애플리케이션&lt;/b&gt; 이라고 합니다. 배치 애플리케이션은 다음 조건을 만족해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 데이터 - 배치 애플리케이션은 대량의 데이터를 가져오거나, 전달하거나, 계산하는 등의 처리를 할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;자동화 - 배치 애플리케이션은 심각한 문제 해결을 제외하고는 사용자 개입이 없이 실행되어야 합니다.&lt;/li&gt;
&lt;li&gt;견고성 - 배치 애플리케이션은 잘못된 데이터를 충돌/중단 없이 처리할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;신뢰성 - 배치 애플리케이션은 무엇이 잘못되었는지를 추적할 수 있어야 합니다.(로깅, 알림)&lt;/li&gt;
&lt;li&gt;성능 - 배치 애플리케이션은 지정한 시간 안에 처리를 완료하거나 동시에 실행되는 다른 애플리케이션을 방해하지 않도록 수행되어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring 진영에서는 배치 애플리케이션을 지원하는 모듈로 Spring Batch가 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 배치 계층 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RjrI1/btsDW8LK8Jj/XcTc8kkjatWrgMVwGar0V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RjrI1/btsDW8LK8Jj/XcTc8kkjatWrgMVwGar0V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RjrI1/btsDW8LK8Jj/XcTc8kkjatWrgMVwGar0V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRjrI1%2FbtsDW8LK8Jj%2FXcTc8kkjatWrgMVwGar0V0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;318&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치는 레이어 구조로 세 개로 구분되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라 레이어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션과 코어 모두 공통 인프라 위에서 빌드됩니다.&lt;/li&gt;
&lt;li&gt;Job 실행의 흐름과 처리를 위한 &lt;b&gt;틀을&lt;/b&gt; 제공합니다.&lt;/li&gt;
&lt;li&gt;개발자와 애플리케이션에서 사용하는 일반적인 Reader와 Writer 그리고 RetryTemplate과 같은 서비스를 포함합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코어 레이어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치 작업을 시작하고 제어하는데 필요한 핵심 런타임 클래스를 포함합니다.&lt;/li&gt;
&lt;li&gt;JobLauncher, Job, Step, Flow&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애플리케이션 레이어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 작성한 모든 배치 작업과 사용자 정의 코드를 포함합니다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치는 계층 구조로 설계되어 있기 때문에 개발자는 Application 계층의 비즈니스 로직에 집중할 수 있습니다. 배치의 동작과 관련된 것은 Batch Core에 있는 클래스들을 이용하여 제어할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 용어 이해와 샘플 배치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OXRL6/btsDXbhm8jO/2krUTv4b3a9oY8yT9aW6A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OXRL6/btsDXbhm8jO/2krUTv4b3a9oY8yT9aW6A0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OXRL6/btsDXbhm8jO/2krUTv4b3a9oY8yT9aW6A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOXRL6%2FbtsDXbhm8jO%2F2krUTv4b3a9oY8yT9aW6A0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1190&quot; height=&quot;604&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job : 하나의 일을 말합니다.&lt;/li&gt;
&lt;li&gt;Step : 하나의 일(Job) 안에서 단계를 의미합니다.&lt;/li&gt;
&lt;li&gt;Tasklet : 하나의 단계(Step) 안에서 실질적으로 수행하는 작업 내용을 의미합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 개념만으로 배치 애플리케이션을 하나 만들어보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration // 하나의 배치 Job을 정의하고 빈으로 등록
class HelloWorldJobConfig(
                private val jobRepository: JobRepository,
                private val transactionManager: PlatformTransactionManager,
                ) {

    @Bean
    fun helloWorldJob(): Job {
        return JobBuilder(&quot;helloWorldJob&quot;, jobRepository)
                .start(helloStep1())
                .build()
    }

    @Bean
    fun helloStep1(): Step {
        return StepBuilder(&quot;helloStep1&quot;, jobRepository) // helloStep1을 생성합니다.
                .tasklet(
                        Tasklet { contribution, chunkContext -&amp;gt;

                // Step의 작업 내용 Tasklet을 정의합니다. 
                println(&quot;hellStep1&quot;)
            // Step은 기본적으로 Tasklet을 무한 반복시킵니다.  
            // 따라서 null이나 RepeatStatus.FINISHED를 반환해줘야 1번만 Tasklet을 실행합니다.
            RepeatStatus.FINISHED
        },
        transactionManager
            )
            .build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job -&amp;gt; Step -&amp;gt; Tasklet&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 설명한 바에 의하면 Job은 위와 같이 구성됩니다. 스프링 배치는 내부적으로 Job이 구성이 되면 Job의 실행 정보와 상태 정보 등의 메타 데이터를 저장하는 JobExecution 클래스가 생성됩니다. Step도 마찬가지로 StepExecution 클래스가 생성됩니다. 이러한 데이터를 담고 있는 클래스들은 DB에 저장되어 현재 Job, Step들의 정보를 보관하게 됩니다. 스프링 배치에서는 위와 같은 클래스의 DB 스키마를 스크립트로 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Batch DB 스키마&lt;/h2&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;880&quot; data-origin-height=&quot;867&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXcczP/btsDUaqhlWt/NkglkJFoJ8Ykm7Q6H5Yovk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXcczP/btsDUaqhlWt/NkglkJFoJ8Ykm7Q6H5Yovk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXcczP/btsDUaqhlWt/NkglkJFoJ8Ykm7Q6H5Yovk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXcczP%2FbtsDUaqhlWt%2FNkglkJFoJ8Ykm7Q6H5Yovk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;880&quot; height=&quot;867&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;867&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 : org.springframework.batch:spring-batch-core&lt;br /&gt;패키지 : org.springframework.batch.core.schema-*.sql&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 위치에서 DB 유형별로 스크립트가 제공됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  batch:
    jdbc:
      initialize-schema: always&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수동 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 위치에서 제공하는 스크립트로 쿼리를 복사 후 직접 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yml에서 spring.batch.jdbc.initialize-schema 속성 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EMBEDDED : &lt;b&gt;내장 DB일 때만&lt;/b&gt; 실행되며 스키마가 자동으로 생성(Default)&lt;/li&gt;
&lt;li&gt;ALWAYS : 스크립트 항상 실행&lt;/li&gt;
&lt;li&gt;NEVER
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스크립트 항상 실행 안 하기 때문에 내장 DB를 사용할 경우 스크립트가 생성이 안되기 때문에 오류 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영에서는 수동으로 스크립트 생성 후 NEVER로 설정하는 것이 권장&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;1047&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNHBSs/btsDXEDEC8Y/KPtJXM5CYf0cKmsWYX6eQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNHBSs/btsDXEDEC8Y/KPtJXM5CYf0cKmsWYX6eQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNHBSs/btsDXEDEC8Y/KPtJXM5CYf0cKmsWYX6eQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNHBSs%2FbtsDXEDEC8Y%2FKPtJXM5CYf0cKmsWYX6eQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1337&quot; height=&quot;1047&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;1047&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마는 왼쪽에 Step 관련 테이블 2개, 오른쪽에 Job 관련 테이블 4개로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BATCH_JOB_INSTANCE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 96px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;필드명&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JOB_INSTANCE_ID&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;고유하게 식별할 수 있는 기본 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;VERSION&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;업데이트 될 때마다 1씩 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JOB_NAME&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;job을 구성할 때 부여하는 job 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JOB_KEY&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;job_name과 jobParameter를 합쳐서 해싱한 값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;job이 실행될 때 JobInstace 정보가 저장됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동일한 job_name과 job_key로 중복 저장될 수 없습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 동일한 job name의 job instance를 만들려면 매번 다른 jobParameter를 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BATCH_JOB_EXECUTION&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드명&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JOB_EXECUTION_ID&lt;/td&gt;
&lt;td&gt;JobExecution을 고유하게 식별하는 기본키, JOB_INSTANCE와 다대일 관계(자신기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VERSION&lt;/td&gt;
&lt;td&gt;업데이트 될 때마다 1씩 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JOB_INSTANCE_ID&lt;/td&gt;
&lt;td&gt;JOB_INSTANCE의 기본 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CREATE_TIME&lt;/td&gt;
&lt;td&gt;실행(Execution)이 생성된 시점을 TimeStamp 형식으로 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;START_TIME&lt;/td&gt;
&lt;td&gt;실행(Execution)이 시작된 시점을 TimeStamp 형식으로 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;END_TIME&lt;/td&gt;
&lt;td&gt;실행(Execution)이 종료된 시점을 TimeStamp 형식으로 기록하며 job 실행 도중 오류가 발생해서 job이 중단된 경우 값이 저장되지 않을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STATUS&lt;/td&gt;
&lt;td&gt;실행 상태(BatchStatus)를 저장(COMPLETED,FAILED,STOPPED..)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXIT_CODE&lt;/td&gt;
&lt;td&gt;실행 종료된(ExitStatus)를 저장(COMPLETED,FAILED..)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXIT_MESSAGE&lt;/td&gt;
&lt;td&gt;Status가 실패일 경우 실패 원인 등의 내용을 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LAST_UPDATED&lt;/td&gt;
&lt;td&gt;마지막 실행(Execution) 시점을 TimeStamp 형식으로 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job의 실행정보가 저장되며 job 생성 시간, 시작 시간, 종료 시간, 실행 상태, 메시지 등을 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BATCH_JOB_EXECUTION_PARAMS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 172px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;필드명&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JOB_EXECUTION_ID&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JobExecution 식별 키, JOB_EXECUTION과 다대일 관계(자신기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;TYPE_CD&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;String, Long, Date, Double 타입 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;KEY_NAME&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;파라미터 키 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;STRING_VAL&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;파라미터 문자 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DATE_VAL&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;파라미터 날짜 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;LONG_VAL&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;파라미터 Long 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DOUBLE_VAL&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;파라미터 Double 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;IDENTIFYING&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;식별 여부(True, False)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;job과 함께 실행되는 JobParameter 정보를 저장합니다.&lt;/li&gt;
&lt;li&gt;job 파라미터는 key-value 형태로 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BATCH_JOB_EXECUTION_CONTEXT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드명&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JOB_EXECUTION_ID&lt;/td&gt;
&lt;td&gt;JobExecution 식별 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHORT_CONTEXT&lt;/td&gt;
&lt;td&gt;Job의 실행 상태 정보, 공유 데이터 등의 정보를 문자열로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZED_CONTEXT&lt;/td&gt;
&lt;td&gt;직렬화(Serialized)된 전체 컨텍스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;job의 실행동안 여러 가지 상태정보, 공유 데이터를 직렬화(Json 형식)하여 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Step에서 해당 데이터를 서로 공유하여 사용합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BATCH_STEP_EXECUTION&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드명&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;STEP_EXECUTION_ID&lt;/td&gt;
&lt;td&gt;Step 실행정보를 고유하게 식별하는 기본 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VERSION&lt;/td&gt;
&lt;td&gt;업데이트 될 때마다 1씩 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STEP_NAME&lt;/td&gt;
&lt;td&gt;Step을 구성할 때 부여하는 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JOB_EXECUTION_ID&lt;/td&gt;
&lt;td&gt;JobExecution의 기본키, JobExecution과 다대일 관계(자신기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;START_TIME&lt;/td&gt;
&lt;td&gt;실행(Execution)이 시작된 시점을 TimeStamp 형식으로 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;END_TIME&lt;/td&gt;
&lt;td&gt;실행이 종료된 시점을 TimeStamp 형식으로 기록하며, job 실행 도중 오류가 발생해서 job이 중단된 경우 값이 저장되지 않을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STATUS&lt;/td&gt;
&lt;td&gt;실행 상태(BatchStatus)를 저장(Completed, Failed, Stopped..)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COMMIT_COUNT&lt;/td&gt;
&lt;td&gt;트랜잭션 당 커밋되는 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ_COUNT&lt;/td&gt;
&lt;td&gt;실행시점에 Read한 Item 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FILTER_COUNT&lt;/td&gt;
&lt;td&gt;실행도중 필터링한 Item 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WRITE_COUNT&lt;/td&gt;
&lt;td&gt;실행도중 저장되고 커밋된 Item 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ_SKIP_COUNT&lt;/td&gt;
&lt;td&gt;실행도중 Read가 Skip된 Item 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WRITE_SKIP_COUNT&lt;/td&gt;
&lt;td&gt;실행도중 Write가 Skip된 Item 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PROCESS_SKIP_COUNT&lt;/td&gt;
&lt;td&gt;실행도중 Process가 Skip된 Item 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROLLBACK_COUNT&lt;/td&gt;
&lt;td&gt;실행도중 rollback이 일어난 수를 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXIT_CODE&lt;/td&gt;
&lt;td&gt;실행종료코드(ExitStatus)를 저장(Completed, Failed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXIT_MESSAGE&lt;/td&gt;
&lt;td&gt;Status가 실패일 경우 실패 원인 등의 내용을 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LAST_UPDATED&lt;/td&gt;
&lt;td&gt;마지막 실행(Execution) 시점을 TimeStamp형식으로 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step의 실행정보가 저장되며 생성 시간, 종료 시간, 실행상태, 메시지 등을 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BATCH_STEP_EXECUTION_CONTEXT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드명&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;STEP_EXECUTION_ID&lt;/td&gt;
&lt;td&gt;StepExecution의 기본 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHORT_CONTEXT&lt;/td&gt;
&lt;td&gt;Step의 실행 상태 정보, 공유 데이터 등의 정보를 문자열로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZED_CONTEXT&lt;/td&gt;
&lt;td&gt;직렬화(Serialized)된 전체 컨텍스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step의 실행동안 여러가지 상태정보, 공유 데이터를 직렬화(Json 형식)하여 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Step 별로 저장되기 때문에 Step 간 서로 공유할 수 없습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Batch 도메인 이해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;739&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PvfQN/btsDRbcnryX/eKsxUFkLNeQUyUOdxHEE5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PvfQN/btsDRbcnryX/eKsxUFkLNeQUyUOdxHEE5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PvfQN/btsDRbcnryX/eKsxUFkLNeQUyUOdxHEE5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPvfQN%2FbtsDRbcnryX%2FeKsxUFkLNeQUyUOdxHEE5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1606&quot; height=&quot;739&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;739&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치 계층 구조에서 가장 상위에 있는 개념으로서 &lt;b&gt;하나의 배치작업 자체를&lt;/b&gt; 의미합니다.&lt;/li&gt;
&lt;li&gt;Job Configuration을 통해 생성되는 객체 단위로서 &lt;b&gt;배치 작업을 어떻게 구성하고 실행할 것인지 전체적으로 설정하고 명세해 놓은 객체입니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Job은 여러 Step을 포함하고 있는 컨테이너로서 반드시 &lt;b&gt;한 개 이상의 Step으로&lt;/b&gt; 구성되어야 합니다.&lt;/li&gt;
&lt;li&gt;배치 Job을 구성하기 위한 최상위 인터페이스이며 스프링 배치가 기본 구현체(SimpleJob, FlowJob)를 제공합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SimpleJob
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순차적으로 Step을 실행시키는 Job&lt;/li&gt;
&lt;li&gt;가장 보편적이고 모든 Job에서 사용할 수 있는 표준 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FlowJob
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 조건과 흐름에 따라 Step을 구성하여 실행시키는 Job&lt;/li&gt;
&lt;li&gt;Flow 객체를 실행시켜서 작업을 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JobInstance&lt;/h4&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;Job이 실행될 때 생성되는 &lt;b&gt;Job의 논리적 실행 단위 객체로서&lt;/b&gt; &lt;b&gt;고유하게 식별 가능한 작업 실행을&lt;/b&gt; 나타냅니다.&lt;/li&gt;
&lt;li&gt;같은 Job이라도 여러 번 실행될 수 있는데 각각의 실행을 구분하기 위한 것을 JobInstance라고 보면 됩니다. (Job : JobInstance = 1:M 관계)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음 시작하는 Job + JobParameters 일 경우 새로운 JobInstance를 생성합니다.&lt;/li&gt;
&lt;li&gt;이전과 동일한 Job + JobParameters으로 실행할 경우 이미 존재하는 JobInstance를 리턴합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부적으로 JobName + jobKey(JobParameters의 해시값)을 통해서 존재하던 JobInstance를 얻습니다.&lt;/li&gt;
&lt;li&gt;이전 수행이 실패했을 경우에는 다시 수행이 가능하지만, 이전 수행이 성공했다면 다시 수행할 수 없습니다.(같은 내용을 반복할 필요가 없기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_JOB_INSTANCE 테이블과 매핑됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JobParameter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Job을 실행할 때 함께 포함되어 사용되는 파라미터를 가진 도메인 객체&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;하나의 Job에 존재할 수 있는 여러 개의 JobInstance를 구분하기 위한 용도&lt;/li&gt;
&lt;li&gt;JobParameters와 JobInstance는 1:1 관계&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION_PARAM 테이블과 매핑됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JobExecution&lt;/h4&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;JobExecution은 &lt;b&gt;JobInstance에 대한 한 번의 시도를 의미하는 객체로서 Job 실행 중에 발생한 정보들을 저장하고 있는 객체입니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;시작시간, 종료시간, 상태(시작됨, 완료, 실패), 종료상태의 속성을 갖습니다. Job은 실행될 때 JobParameters를 받아서 JobInstance를 생성하고 JobInstance가 실행될 때마다 JobExecution이 생성됩니다.&lt;/li&gt;
&lt;li&gt;JobInstance는 같은 파라미터에 대해서 단 한 번만 실행 가능하다고 했지만 JobExecution이 JobInstance가 실행될 때마다 생성될 수 있는 이유는 JobInstance의 상태에 따라 예외 케이스가 있기 때문입니다.&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION 테이블과 매핑됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JobExecution과 JobInstance와의 관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobExecution은 'FAILED' 또는 'COMPLETED' 등의 Job의 실행 상태를 갖고 있습니다.&lt;/li&gt;
&lt;li&gt;JobExecution의 실행 상태 결과가 'COMPLETED'면 JobInstance 실행이 완료된 것으로 간주해서 재실행이 불가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobExecution의 실행 상태 결과가 'FAILED'면 JobInstance 실행이 완료되지 않은 것으로 간주해서 재실행이 가능합니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobParameters가 동일한 값으로 Job을 실행할지라도 이전의 JobExecution이 실패했기 때문에 기존의 JobInstance에서 &lt;b&gt;새로운 JobExecution을 생성하여 실행이 이뤄집니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, JobExecution의 실행 상태 결과가 'COMPLETE' 될 때까지 하나의 JobInstance 내에서 JobExecution이 생성될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step&lt;/h3&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;1446&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TyZ2A/btsDW767Kx4/YAJ81mQzpljM5F10PmbyzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TyZ2A/btsDW767Kx4/YAJ81mQzpljM5F10PmbyzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TyZ2A/btsDW767Kx4/YAJ81mQzpljM5F10PmbyzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTyZ2A%2FbtsDW767Kx4%2FYAJ81mQzpljM5F10PmbyzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1446&quot; height=&quot;758&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Batch Job을 구성하는 독립적인 하나의 단계로서, 실제 배치 처리를 정의하고 컨트롤하는데 필요한 모든 정보를 가지고 있는 도메인 객체&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;단순한 단일 태스크뿐 아니라 입력과 처리 그리고 출력과 관련된 복잡한 비즈니스 로직을 포함하는 모든 설정들을 담고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 Job은 하나 이상의 Step으로 구성됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Step 인터페이스는 Step을 실행하는 execute 메서드를 갖고 있고, 구현체로 AbstractStep이 있습니다. 그리고 이를 상속받은 4가지 Step이 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TaskletStep
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 기본이 되는 클래스로서 Tasklet 타입의 구현체들을 제어합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PartitionStep
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티 스레드 방식으로 Step을 여러 개로 분리해서 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JobStep
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step 내에서 Job을 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FlowStep
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step 내에서 Flow를 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;StepExecution&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Step에 대한 한 번의 시도를 의미하는 객체로서 Step 실행 중에 발생한 정보들을 저장하고 있는 객체&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시작시간, 종료시간, 상태, commit count, rollback count 등의 속성을 갖습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step이 &lt;b&gt;매번 시도될 때마다 새로 생성되며&lt;/b&gt; &lt;b&gt;각 Step 별로 생성됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Job이 실패되어 재시작될 경우 이미 성공적으로 완료된 Step은 재실행되지 않고 실패한 Step만 실행됩니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모두 재시작할 수 있는 옵션도 존재합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이전 단계 Step이 실패해서 현재 Step을 실행되지 않았다면 StepExecution은 생성되지 않습니다.&lt;/li&gt;
&lt;li&gt;JobExecution과의 관계
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobExecution과 StepExecution은 1:M 관계&lt;/li&gt;
&lt;li&gt;Step의 StepExecution이 모두 정상적으로 완료되야만 JobExecution이 정상적으로 완료됩니다.&lt;/li&gt;
&lt;li&gt;Step의 StepExecution 중 하나라도 실패하면 JobExecution은 실패합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_STEP_EXECUTION 테이블에 매핑됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;StepContribution&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;청크 프로세스의 변경 사항을 버퍼링 한 후 StepExecution 상태를 업데이트하는 도메인 객체&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쉽게 말하면, Step에서 정의한 일들을 처리한 결과들을 저장해 두다가 StepExecution에 업데이트하는 일을 말합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;청크 커밋 직전에 StepExecution의 apply 메서드를 호출하여 상태를 업데이트합니다.&lt;/li&gt;
&lt;li&gt;ExitStatus의 기본 종료코드 외 사용자 정의 종료코드를 생성해서 적용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 과정&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1543&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b33JJD/btsDWHt7t92/2kvYKDc6ebZtFK9vQxHxOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b33JJD/btsDWHt7t92/2kvYKDc6ebZtFK9vQxHxOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b33JJD/btsDWHt7t92/2kvYKDc6ebZtFK9vQxHxOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb33JJD%2FbtsDWHt7t92%2F2kvYKDc6ebZtFK9vQxHxOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1543&quot; height=&quot;804&quot; data-origin-width=&quot;1543&quot; data-origin-height=&quot;804&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;잡이 실행되면 TaskletStep에서 StepExecution을 생성합니다.&lt;/li&gt;
&lt;li&gt;StepExecution은 StepContribution을 만듭니다.&lt;/li&gt;
&lt;li&gt;Chunk 기반 Tasklet이 실행됩니다.&lt;/li&gt;
&lt;li&gt;청크 프로세스의 데이터들이 StepContribution에 쌓입니다.&lt;/li&gt;
&lt;li&gt;StepExecution이 완료되는 시점에 apply 메서드를 호출하여 StepContribution의 필드 값들을 StepExecution에 업데이트시킵니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ExecutionContext&lt;/h3&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;996&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buxsed/btsDSpath1n/XYaReOCRY9hKQ8gRkC81m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buxsed/btsDSpath1n/XYaReOCRY9hKQ8gRkC81m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buxsed/btsDSpath1n/XYaReOCRY9hKQ8gRkC81m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuxsed%2FbtsDSpath1n%2FXYaReOCRY9hKQ8gRkC81m1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;996&quot; height=&quot;199&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;199&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프레임워크에서 유지 및 관리하는 &quot;key-value&quot;으로 된 컬렉션으로 StepExecution 또는 JobExecution 객체의 상태(필드값들)를 저장하는 공유 객체&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;DB에 직렬화 한 값으로 저장됩니다.(&quot;key&quot;:&quot;value&quot;)&lt;/li&gt;
&lt;li&gt;공유 범위
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step 범위 : 각 Step Execution에 저장되며 &lt;b&gt;Step 간 서로 공유 불가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Job 범위 : 각 Job의 Execution에 저장되며 Job 간 서로 공유는 되지 않지만, &lt;b&gt;해당 Job에 속한 Step은 공유 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Job 재시작 시 이미 처리한 Row 데이터는 건너뛰고 이후부터 수행하도록 상태 정보를 활용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ozxlh/btsDWscRDtW/zTMRteKIrGkL6jd0MICEe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ozxlh/btsDWscRDtW/zTMRteKIrGkL6jd0MICEe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ozxlh/btsDWscRDtW/zTMRteKIrGkL6jd0MICEe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOzxlh%2FbtsDWscRDtW%2FzTMRteKIrGkL6jd0MICEe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;233&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;233&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;JobExecution과 StepExecution 각각 필드값으로 ExecutionContext를 갖고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ExecutionContextTasklet implements Tasklet {
    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        ExecutionContext jobExecutionContext = contribution.getStepExecution().getJobExecution().getExecutionContext();
        ExecutionContext stepExecutionContext = contribution.getStepExecution().getExecutionContext();

        ExecutionContext jobExecutionContext2 = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext();
        ExecutionContext stepExecutionContext2 = chunkContext.getStepContext().getStepExecution().getExecutionContext();
        return RepeatStatus.FINISHED;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자 두 개에서 모두 꺼내서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JobRepository&lt;/h3&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;882&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxVm2G/btsDUacKq5E/c7hfHFSGBhhmvn3x11KyKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxVm2G/btsDUacKq5E/c7hfHFSGBhhmvn3x11KyKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxVm2G/btsDUacKq5E/c7hfHFSGBhhmvn3x11KyKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxVm2G%2FbtsDUacKq5E%2Fc7hfHFSGBhhmvn3x11KyKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;882&quot; height=&quot;519&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;519&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;배치 작업 중의 정보를 저장하는 저장소 역할&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Job이 언제 수행되었고, 언제 끝났으며, 몇 번이 실행되었고 실행에 대한 결과 등의 배치 작업의 수행과 관련된 모든 메타 데이터를 저장합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobLauncher, Job, Step 구현체 내부에서 CRUD 기능을 처리합니다.&lt;/li&gt;
&lt;li&gt;실행 과정에서 데이터를 DB에 저장하고, 필요한 데이터는 읽어오는 작업을 하는 객체라고 보면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JobLuancher&lt;/h3&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;1400&quot; data-origin-height=&quot;823&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgzEvP/btsDQ7nu13L/kzkZlGzGcHOBhT5QMkiCt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgzEvP/btsDQ7nu13L/kzkZlGzGcHOBhT5QMkiCt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgzEvP/btsDQ7nu13L/kzkZlGzGcHOBhT5QMkiCt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgzEvP%2FbtsDQ7nu13L%2FkzkZlGzGcHOBhT5QMkiCt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;823&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;823&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;배치 Job을 실행시키는 역할&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Job과 Job Parameters를 인자로 받아 배치 작업을 수행한 후 최종 client에게 JobExecution을 반환합니다.&lt;/li&gt;
&lt;li&gt;스프링 부트 배치가 구동되면 JobLauncher 빈이 &lt;b&gt;자동 생성&lt;/b&gt; 됩니다.&lt;/li&gt;
&lt;li&gt;Job 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobLauncher.run(job, jobParameters)&lt;/li&gt;
&lt;li&gt;스프링 부트 배치에서는 jobLauncherApplicationRunner가 자동적으로 JobLuancher을 실행시킵니다.&lt;/li&gt;
&lt;li&gt;동기적 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;taskExecutor를 SyncTaskExecutor로 설정한 경우(기본값)&lt;/li&gt;
&lt;li&gt;JobExecution을 획득하고 배치 처리를 최종 완료한 이후 Client에게 JobExecution을 반환&lt;/li&gt;
&lt;li&gt;스케줄러에 의한 배치처리에 적합(배치처리시간이 길어도 무관한 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비동기적 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;taskExecutor가 SimpleAsyncTaskExecutor로 설정할 경우&lt;/li&gt;
&lt;li&gt;JobExecution을 획득한 후 Client에게 바로 JobExecution을 반환하고 배치처리를 진행&lt;/li&gt;
&lt;li&gt;HTTP 요청에 의한 배치처리에 적합(배치처리 시간이 길 경우 응답이 늦어지지 않도록 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>spring</category>
      <category>Spring Batch</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/64</guid>
      <comments>https://backtony.tistory.com/64#entry64comment</comments>
      <pubDate>Wed, 24 Jan 2024 23:26:05 +0900</pubDate>
    </item>
    <item>
      <title>NGINX - CORS 처리하기</title>
      <link>https://backtony.tistory.com/63</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처(Origin)이란?&lt;/h2&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;859&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpllHB/btsDGLdBFJP/KBFQDs0lkzW0owToGNkCek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpllHB/btsDGLdBFJP/KBFQDs0lkzW0owToGNkCek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpllHB/btsDGLdBFJP/KBFQDs0lkzW0owToGNkCek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpllHB%2FbtsDGLdBFJP%2FKBFQDs0lkzW0owToGNkCek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;859&quot; height=&quot;200&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처(Origin)은 Scheme(protocol), Host, Port로 구성되어 있습니다. http, https의 경우에는 프로토콜이 포트 번호를 포함하고 있기 때문에 생략 가능하지만, 만약 포트를 명시함다면 포트번호까지 일치해야 출처가 같다고 볼 수 있습니다. 즉, scheme, host, port가 모두 일치하는 경우를 동일한 출처로 봅니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 기준이 되는 origin
https://www.backtony.github.io 

# 같은 origin
https://www.backtony.github.io/about 
https://www.backtony.github.io/about?q=hello
https://user:password@www.backtony.github.io

# 다른 origin
http://www.backtony.github.io # 스킴이 다름
https://cktony.github.io # 호스트가 다름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SOP - 동일 출처 정책&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹에는 크게 SOP(Same Origin Policy)와 CORS(Cross Origin Rescure Sharing) 두 가지 정책이 있습니다. SOP는 &lt;b&gt;같은 출처(Origin)에서만 리소스를 공유할 수 있다&lt;/b&gt;라는 규칙을 갖는 보안 정책입니다. 앞서 언급한 출처(Origin)이 같다면 같은 출처로 인정되고, 자신의 출처와 다를 경우 브라우저는 교차출처 요청을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 봅시다. 프론트엔드 개발자가 React로 개발하고 백엔드 개발자는 spring으로 개발하는 경우, 백엔드 서버와 프론트가 별도로 존재합니다. 프론트 URL이 http://localhost:3000 이고, 백엔드 URL이 http//localhost:8080 이라면 프론트와 백엔드는 서로 다른 출처(Origin)으로써 SOP를 위반하기 때문에 서버로부터 응답이 브라우저로 넘어갈때 브라우저에서 CORS policy 오류를 발생시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, scheme, host, port 모두 일치해야 Same Origin(같은 출처)이며, 이들 중 하나라도 일치하지 않으면 Corss Origin이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CORS - 교차 출처 리소스 공유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS는 Cross-Origin Resource Sharing(교차 출처 리소스 공유)의 줄임말로, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제를 의미합니다. 웹 애플리케이션은 리소스(데이터를 가저오는 곳)가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CORS 동작 방식&lt;/h2&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;1174&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUrIrb/btsDIySm66P/Y0mhZzoCkR3XhgikKwZNUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUrIrb/btsDIySm66P/Y0mhZzoCkR3XhgikKwZNUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUrIrb/btsDIySm66P/Y0mhZzoCkR3XhgikKwZNUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUrIrb%2FbtsDIySm66P%2FY0mhZzoCkR3XhgikKwZNUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1174&quot; height=&quot;468&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션은 다른 출처 리소스를 요청할 때, HTTP 프로토콜을 사용하는데 브라우저는 요청 헤더에 Origin 필드로 요청을 보내는 출처를 함께 담아 보냅니다.&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;982&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6Dybh/btsDJCtbqZm/kgJSKPuPqjlgcX9SzfWiRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6Dybh/btsDJCtbqZm/kgJSKPuPqjlgcX9SzfWiRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6Dybh/btsDJCtbqZm/kgJSKPuPqjlgcX9SzfWiRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6Dybh%2FbtsDJCtbqZm%2FkgJSKPuPqjlgcX9SzfWiRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;982&quot; height=&quot;830&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 요청에 대한 응답을 보낼 때, 응답 헤더의 Access-Control-Allow-Origin 헤더에 &lt;b&gt;리소스 접근에 허용된 출처&lt;/b&gt;를 내려주고, 이후 응답을 받은 브라우저는 보냈던 요청의 Origin과 서버 응답의 Access-Control-Allow-Origin 헤더 값을 비교하여 이 응답이 유효한 응답인지 아닌지를 결정합니다. 기본적인 흐름은 위와 같지만, CORS가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경됩니다.&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;649&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVTVVW/btsDKKxr2k8/WvzWJu3jxS48jZMts57TcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVTVVW/btsDKKxr2k8/WvzWJu3jxS48jZMts57TcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVTVVW/btsDKKxr2k8/WvzWJu3jxS48jZMts57TcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVTVVW%2FbtsDKKxr2k8%2FWvzWJu3jxS48jZMts57TcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;207&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS는 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않습니다. 예를 들어, spring 서버 안에서 restTemplate을 사용해서 다른 서버로 요청을 보내는 것은 CORS가 발생하지 않습니다. 그리고 일반적으로 CORS 정책을 위반하는 리소스 요청을 하더라도 해당 서버는 정상적으로 응답을 하고, 이후 브라우저가 응답을 분석해서 CORS 정책 위반이라고 판단되면 응답을 사용하지 않고 버립니다. 이는 실제로 서버가 아닌 브라우저에 구현된 스펙이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Preflight Request(예비 요청)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 요청을 보내기 전에 사전에 예비 요청을 보내고 허용 된다면 실제 요청을 보내는 방식입니다. 사전 요청을 보낼 때는 HTTP 메서드 중 Option 메서드를 사용합니다. 기본적으로 본 요청의 HTTP 메서드가 PUT, DELETE 일 경우 사용됩니다. put이나 delete는 서버의 데이터를 변경하는 요청이기 때문에, 요청을 보내기 전에 예비 요청을 보내서 우선 인증부터 하고 본 요청을 받아 서버에서 코드가 동작하게 하는 원리입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9cCJq/btsDJaw96Ci/Vp4cwdBQJRhWf2jJqMDKzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9cCJq/btsDJaw96Ci/Vp4cwdBQJRhWf2jJqMDKzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9cCJq/btsDJaw96Ci/Vp4cwdBQJRhWf2jJqMDKzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9cCJq%2FbtsDJaw96Ci%2FVp4cwdBQJRhWf2jJqMDKzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;510&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;510&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;js로 fetch API를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내립니다.&lt;/li&gt;
&lt;li&gt;브라우저는 서버로 HTTP Method Option으로 예비 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;서버는 예비 요청에 대한 응답으로 Access-Control-Allow-Origin 헤더를 통해 현재 서버는 어떤 출처를 허용하고 있는지 알려줍니다.&lt;/li&gt;
&lt;li&gt;브라우저는 자신이 보낸 예비 요청과 서버가 응답에 담아준 허용 정책을 비교한 후, 이 요청을 보내는 것이 안전하다고 판단되면 같은 엔드포인트로 다시 본 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;이후 서버가 본 요청에 대한 응답을 주면서 브라우저는 최종적인 응답 데이터를 자바스크립트에게 넘겨줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzD25I/btsDGXrNzC8/FslHwtZkIkalC5IkolQXF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzD25I/btsDGXrNzC8/FslHwtZkIkalC5IkolQXF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzD25I/btsDGXrNzC8/FslHwtZkIkalC5IkolQXF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzD25I%2FbtsDGXrNzC8%2FFslHwtZkIkalC5IkolQXF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1214&quot; height=&quot;1518&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;option의 요청 헤더들을 보면 단순히 Origin에 대한 정보 뿐만 아니라 자신이 예비 요청 이후에 보낼 본 요청에 대한 다른 정보들도 함께 포함되어 있는 것을 볼 수 있습니다. 위 예비 요청에서는 브라우저는 Access-Control-Requeset-Method를 통해 이후 Delete 메서드를 사용할 것을 서버에 미리 알려주고 있습니다.&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;1316&quot; data-origin-height=&quot;1350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bImNhL/btsDJkM3GTf/9mJwnmcKVOTeEBJxFbfFxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bImNhL/btsDJkM3GTf/9mJwnmcKVOTeEBJxFbfFxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bImNhL/btsDJkM3GTf/9mJwnmcKVOTeEBJxFbfFxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbImNhL%2FbtsDJkM3GTf%2F9mJwnmcKVOTeEBJxFbfFxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1316&quot; height=&quot;1350&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 예비 요청인 Option 메서드의 응답 헤더를 봅시다. Access-Control-Allow-Origin 헤더는 http://localhost:3000 으로 내려왔으므로 해당 서버는 localhost:3000(외부 출처)에서의 접근은 허용한다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순 요청(Simple Request)&lt;/h3&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;764&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5CSTq/btsDGJGRAhW/ESQ8PYVLmKokDnSyHCa5Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5CSTq/btsDGJGRAhW/ESQ8PYVLmKokDnSyHCa5Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5CSTq/btsDGJGRAhW/ESQ8PYVLmKokDnSyHCa5Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5CSTq%2FbtsDGJGRAhW%2FESQ8PYVLmKokDnSyHCa5Vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;764&quot; height=&quot;316&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 요청은 예비 요청(Preflight) 없이 바로 본 요청을 서버로 보낸 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS 정책 위반 여부를 검사하는 방식입니다. 단순 요청은 예비요청을 아래 3가지 경우를 만족할 때만 가능합니다.&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;요청 메서드는 GET, HEAD, POST 중 하나여야만 합니다.(Put, Delete는 무조건 Preflight)&lt;/li&gt;
&lt;li&gt;유저 에이전트가 자동으로 설정한 헤더 외에, 수동으로 설정할 수 있는 헤더는 Fetch 명세에서 &lt;code&gt;CORS-safelisted request-header&lt;/code&gt;로 정의되어 있는 다음 헤더만 사용할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Content-Type을 사용하는 경우에 다음 값만 허용됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application/x-www.form-urlencoded&lt;/li&gt;
&lt;li&gt;multipart/form-data&lt;/li&gt;
&lt;li&gt;text/plain&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 조건들이 까다롭기 때문에 단순요청을 보내는 것은 쉽지 않습니다. POST, GET 요청이더라도 대부분 application/json으로 통신하기 때문에 3번째 content-type 조건에 위반됩니다. 따라서 대부분 예비 요청방식으로 이뤄지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증된 요청(Credentialed Request)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예비요청에서 보안을 더 강화하고 싶을 때 사용하는 방법입니다. 인증된 요청 역시 예비 요청처럼 Preflight가 먼저 일어납니다. 기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않습니다. 따라서 요청에 인증과 관련된 정보(쿠키)를 담을 수 있게 해주는 옵션이 있는데 credentials 옵션으로 3가지가 있습니다.&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;same-origin(기본값) : 같은 출처 간 요청에만 인증 정보를 담을 수 있다.&lt;/li&gt;
&lt;li&gt;include : 모든 요청에 인증 정보를 담을 수 있다.&lt;/li&gt;
&lt;li&gt;omit : 모든 요청에 인증 정보를 담지 않는다.&lt;/li&gt;
&lt;li&gt;fetch를 사용하지 않고 axios를 사용할 땐 withCredentials:true 옵션을 사용하여 쿠키를 전송할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 credentials 옵션값인 same-origin이나 include와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 브라우저는 다른 출처의 리소스를 요청할 때 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;만 확인하는 것이 아니라 다른 조건을 추가로 검사하게 됩니다. 예를 들어 credentails 옵션을 사용하여 요청에 인증정보가 담겨있는 상태에서 다른 출처의 리소스를 요청하게 되면, 브라우저는 CORS 정책 위반 여부를 검사하는 룰에 다음 두가지를 추가합니다.&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;Access-Controll-Allow-Origin에는 모든 요청을 허용하는 *를 사용할 수 없으며 명시적인 URL이 사용되어야 한다.&lt;/li&gt;
&lt;li&gt;응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 존재해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CORS 관련 HTTP Header 정리&lt;/h2&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;request Header
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Origin : cross-site 접근 요청 혹은 사전 전달 요청 출처&lt;/li&gt;
&lt;li&gt;Access-Control-Request-Method : preflight 사전 요청에서 본 요청 때 어떤 HTTP method를 사용할 것인지 서버에 알리기 위한 것&lt;/li&gt;
&lt;li&gt;Access-Control-Request-Headers : 사전 요청에서 본 요청 때 어떤 HTTP header가 사용될 것인지 서버에 알리기 위한 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;response Header
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access-Control-Allow-Origin : origin는 리소스에 접근 가능케 하는 URI를 의미한다.&lt;/li&gt;
&lt;li&gt;Access-Control-Expose-Headers : cross-origin 요청에 대한 응답인 경우, 해당 응답의 헤더 중에서 브라우저의 스크립트(Java Script...등)가 접근가능한 헤더를 지정하는데 사용&lt;/li&gt;
&lt;li&gt;Access-Control-Allow-Credentials : credentials 사용 자격 요건에 충족되었는지 여부로 사용되거나 사전 요청에 대한 응답으로 본 요청을 수행할지 여부를 판단&lt;/li&gt;
&lt;li&gt;Access-Control-Allow-Methods : 허용되는 Method&lt;/li&gt;
&lt;li&gt;Access-Control-Allow-Headers : 클라이언트에서 서버로 보낼 수 있는 허용되는 헤더&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Nginx로 CORS 해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cors 정책에 따라 브라우저는 요청 헤더에 origin이라는 필드에 요청을 보내는 출처를 함께 담아보냅니다. 서버는 응답에서 access-control-allow-origin 헤더에 접근을 허용하는 출처를 응답하고 브라우저는 자신이 보낸 요청의 origin과 서버에서 내려준 access-control-allow-origin을 비교해보고 응답이 유효한지 아닌지 판단합니다. 이를 이용해 처리해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cors에 관한 내용들을 담은 파일들을 별도로 만들고 nginx.conf 설정에서 필요한 곳에서 include하는 형식으로 처리해보겠습니다.&lt;/p&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;cors-header.conf&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# proxy_hide_header 옵션은 백엔드 서버에서 응답한 헤더를 제거하는 역할
# 서버에서 내려온 cors 설정 헤더들을 숨겨버린다. -&amp;gt; nginx에서 설정할 것이기 때문
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;

# add_header는 응답 헤더를 추가해주는 역할
# nginx에서 응답에 cors에 필요한 헤더를 넣어줍니다.
# always 옵션은 모든 조건의 응답에 대해 이 응답 헤더를 포함한다는 의미이다. 이 설정을 하지 않을 경우, 2XX에 해당하는 성공 응답에만 응답 헤더가 포함됩니다.
add_header 'Access-Control-Allow-Origin' $allow_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Headers' $http_access_control_request_headers always;&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;cors-options-response.conf&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' $allow_origin;
    add_header 'Access-Control-Allow-Credentials' 'true';

    # preflight에 access-control-request-headers 헤더 값 그대로 반환
    # https://www.geeksforgeeks.org/http-headers-access-control-request-headers/
    add_header 'Access-Control-Allow-Headers' $http_access_control_request_headers;
    add_header 'Access-Control-Allow-Methods' 'GET,HEAD,POST,PUT,DELETE';
    add_header 'Access-Control-Max-Age' 86400;
    add_header 'Content-Length' 0;

    return 204;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 메서드가 option이면 preflight 요청이므로 실제 서버로 요청을 보내지 않고 nginx에서 자체적으로 처리하도록 하는 설정입니다. allow_origin 변수는 nginx.conf 파일에서 허용할 origin을 명시할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;nginx.conf&lt;/b&gt;&lt;br /&gt;기타 옵션들은 제외하고 cors를 적용하기 위한 설정만 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;worker_processes  1;

events {
    worker_connections  1024;
    use epoll;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    charset utf-8;

    upstream cors_service {
        server localhost:8080;
        keepalive 100;
    }

    # cors에서 허용할 url들 명시해서 매칭되면 origin_allowed 변수를 1로 세팅
    map $http_origin $origin_allowed {
        default 0;
        http://localhost:3000 1;
    }

    # origin_allowed가 1이면 허용된 url이므로 allow_origin 변수에 현재 요청으로 들어온 host_origin 값을 세팅
    # cors-options-response.conf 파일에서 사용된 allow_origin 변수가 여기서 세팅되는 것
    map $origin_allowed $allow_origin {
        default &quot;&quot;;
        1 $http_origin;
    }

    server {
        listen       80 default_server;

        # for console!
        access_log /dev/stdout combined;
        error_log /dev/stderr info;

        location / {
            include common/cors-header.conf;
            include common/cors-options-response.conf;

            proxy_pass http://cors_service;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>NGINX</category>
      <category>CORS</category>
      <category>nginx</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/63</guid>
      <comments>https://backtony.tistory.com/63#entry63comment</comments>
      <pubDate>Sat, 20 Jan 2024 14:37:09 +0900</pubDate>
    </item>
    <item>
      <title>NGINX - HTTP 모듈 구성</title>
      <link>https://backtony.tistory.com/62</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Nginx 기본 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        access_log  logs/host.access.log  combined;

        location / {
            root   html;
            index  index.html index.htm;
        }


        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

    }


}&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;nginx.conf의 기본 파일 경로는 &lt;b&gt;&lt;code&gt;/usr/local/nginx/conf/nginx.conf&lt;/code&gt;&lt;/b&gt;에 존재합니다. 위 코드는 기본 설정 파일에서 주석들을 걸러내고 몇 가지 수정한 코드입니다. 하나씩 살펴봅시다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP 핵심 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 모듈 : &lt;a href=&quot;https://runebook.dev/ko/docs/nginx/http/ngx_http_core_module&quot;&gt;https://runebook.dev/ko/docs/nginx/http/ngx_http_core_module&lt;/a&gt;?&lt;br /&gt;모듈 변수 : &lt;a href=&quot;http://nginx.org/en/docs/http/ngx_http_core_module.html#variables&quot;&gt;http://nginx.org/en/docs/http/ngx_http_core_module.html#variables&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 핵심 모듈에는 여러 가지 모듈 지시어가 있고 기본적으로 제공되는 많은 변수들을 가지고 있어 지시어의 값으로도 사용할 수 있습니다. 변수가 허용되지 않는 지시어의 값에 변수를 사용하면 아무런 오류 없이 변수 이름을 그대로 문자로 사용하므로 주의가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지시어 값의 축약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;k 또는 K : 킬로바이트&lt;/li&gt;
&lt;li&gt;m 또는 M : 메가바이트&lt;/li&gt;
&lt;li&gt;g 또는 G : 기가바이트&lt;/li&gt;
&lt;li&gt;ms : 밀리초&lt;/li&gt;
&lt;li&gt;s : 초(기본적인 시간 단위)&lt;/li&gt;
&lt;li&gt;m : 분&lt;/li&gt;
&lt;li&gt;h : 시간&lt;/li&gt;
&lt;li&gt;d : 일&lt;/li&gt;
&lt;li&gt;w : 주&lt;/li&gt;
&lt;li&gt;M : 달&lt;/li&gt;
&lt;li&gt;y : 년&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 둘다 동일한 의미
client_max_body size 2G;
client_max_body size 2048M;

# 동일한 의미
client_body_timeout 180;
client_body_timeout 3m;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 결합해서 사용 가능
client_body_timeout 1m30s;

# 값을 띄어쓰기로 구분하는 경우 따옴표 필요
client_body_timeout '1m 30s 500ms';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈은 지시어 값을 정의할 때 사용할 수 있는 변수를 제공합니다.&lt;/li&gt;
&lt;li&gt;변수는 항상 &lt;code&gt;**$**&lt;/code&gt;로 시작합니다.&lt;/li&gt;
&lt;li&gt;log_format 지시어를 설정할 때 형식 문자열에 모든 종류의 변수를 포함시킬 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;location ^~ /admin/ {
    access_log logs/main.log;
    log_format main '$pid - $nginx_version -$remote_addr';
}&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;하지만 일부 지시어는 변수를 사용할 수 없습니다. 아래 error_log에서는 변수가 치환되지 않고 그대로 문자가 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;error_log logs/error-$nginx_version.log;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문자열 값&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지시어 값으로 사용할 수 있는 문자열은 세 가지 형식이 있습니다.&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;따옴표 없이 입력
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;root /home/example.com/www;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;공백이나 세미콜론(;) 또는 중괄호({}) 같은 특수문자를 사용하고 싶을 때는 앞에 역슬래시를 문자 앞에 붙이거나, 전체 문자열을 작은따옴표나 큰따옴표로 묶어야 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;root '/home/example.com/my web pages';&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;엔진엑스에서는 작은따옴표와 큰따옴표는 아무런 차이가 없습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt;앞에 역슬래시를 붙이지만 않는다면 문자열 안에서 삽입된 변수는 정상적으로 확장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;worker&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;worker_processes  1;

events {
    worker_connections  1024;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;worker_processes 1;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업자 프로세스 하나만 시작하게 하는데, 모든 요청이 하나의 실행 경로로 처리되며, CPU 코어 하나로 실행됨을 의미합니다.&lt;/li&gt;
&lt;li&gt;이 값은 CPU 코어당 최소 하나의 프로세스를 갖도록 설정하는 것이 권장됩니다.&lt;/li&gt;
&lt;li&gt;값을 auto로 주는 경우 엔진엑스가 최적의 값을 결정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;worker_connections 1024;
&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;예를 들어 각각 1024개의 연결을 수용하는 작업자 프로세스가 4개라면 서버는 동시 연결을 최대 4096개까지 처리하게 됩니다.&lt;/li&gt;
&lt;li&gt;이 설정은 보유한 하드웨어에 맞게 조정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cf) 엔진엑스 프로세스 아키텍처&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔진엑스를 시작하면 유일한 프로세스인 주 프로세스가 생기는데, 현재 사용자와 그룹의 권한으로 실행됩니다. 시스템이 부딩될 때 init 스크립트로 엔진엑스 서비스가 실행되면 보통 root 사용자와 root 그룹 권한을 가집니다. 주 프로세스는 클라이언트의 요청을 스스로 처리하지는 않고 대신 그 일을 처리해 줄 작업자 프로세스를 만듭니다. 작업자 프로세스는 별도로 정의한 사용자와 그룹으로 실행될 수 있으며 작업자 프로세스의 수, 작업자 프로세스당 최대 연결 수, 작업자 프로세스를 실행하는 사용자와 그룹 등을 구성파일로 정의할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조 블록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;http {
    include       mime.types;
    gzip  on;

    server {
        listen       80;
        server_name  localhost;

        location /downloads/ {
            gzip off;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;include
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;include 지시어는 이름 그대로 지정된 파일을 포함시킵니다.&lt;/li&gt;
&lt;li&gt;별도의 구성을 재사용하기 위해 파일로 분리하고 가져올 때 사용됩니다.&lt;/li&gt;
&lt;li&gt;mime.types 과 같이 사전에 nginx에서 정의된 것들을 가져올 수도 있습니다.&lt;/li&gt;
&lt;li&gt;와일드 카드를 사용하여 한번에 include 시킬 수 있습니다. 단일 파일 include의 경우 파일이 없으면 검증에 실패하지만 와일드카드의 경우 파일이 있든 없든 통과합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;http
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 블록은 구성 파일의 최상위에 삽입됩니다.&lt;/li&gt;
&lt;li&gt;http와 관련된 모듈 지시어와 블록은 http 블록에만 정의할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;server
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 사이트 하나를 선언할 수 있는 블록입니다.&lt;/li&gt;
&lt;li&gt;엔진엑스가 특정 웹사이트(하나 이상의 호스트 이름, 예를 들어, &lt;a href=&quot;http://www.website.com&quot;&gt;www.website.com&lt;/a&gt; 같은 이름으로 식별됨)을 인식하고 그 구성을 얻는 블록입니다.&lt;/li&gt;
&lt;li&gt;http 블록 안에서만 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;location
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 경로에 따른 처리를 정의합니다.&lt;/li&gt;
&lt;li&gt;웹 사이트의 특정 위치에만 적용되는 설정을 정의하는 데 쓰는 블록입니다.&lt;/li&gt;
&lt;li&gt;이 블록은 server 블록 안이나 다른 location 블록 안에 중첩해서 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;listen&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;listen [주소][:포트] [추가옵션];
# ex    
listen 127.0.0.1:8080;
listen 127.0.0.1;
listen 80 default_server;
listen 443 ssl;
listen 443 ssl http2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 나열된 지시어로 가상 호스트를 구성할 수 있습니다. 가상 호스트는 호스트 이름이나 ip주소와 포트의 조합으로 식별되는 server 블록을 만들어 실현됩니다. 웹 사이트를 제공하는 소켓을 여는 데 사용되는 ip 주소나 포트, 또는 두 가지 모두를 지정할 수 있습니다. 그리고 아래와 같은 추가 옵션을 지정할 수 있습니다.&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;default_server : 해당 server 블록을 지정된 ip 주소와 포트로 들어온 모든 요청의 기본 웹 사이트로 지정&lt;/li&gt;
&lt;li&gt;ssl : 웹 사이트가 SSL을 통해 제공되도록 지정&lt;/li&gt;
&lt;li&gt;http2 : http_v2 모듈이 있을 경우 HTTP/2 프로토콜을 지원하도록 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;server_name&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server_name 호스트이름1 [호스트이름2...];
# ex
server_name www.website1.com;
server_name *www.website1*.com;
server_name www.website1.com www.website2.com;
seveer_name ~^(www)\.example\.com$&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;server 블록에 하나 이상의 호스트 이름을 할당하는 지시어입니다. 엔진엑스는 HTTP 요청을 받을 때 요청의 Host 헤더를 server 블록 모두와 비교합니다. 그중 호스트 이름과 맞는 첫 번째 server 블록이 선택됩니다. 만약 여러 블록이 같은 listen 지시어를 가지고 있다면 server_name은 요청이 전달될 서버 블록을 찾는 역할을 합니다. 매칭되는 server_name이 없다면 listen의 default_server에 해당하는 server 블록으로 매칭됩니다. 이 블록은 정규식과 와일드카드를 사용할 수 있습니다. 정규식을 사용할 때 호스트 이름은 &lt;code&gt;~&lt;/code&gt;문자로 시작해야 합니다. 지시어 값에 빈 문자열을 사용해 host 헤더 없이 들어오는 모든 요청을 받게 할 수도 있습니다. 다만 적어도 하나의 정규 호스트 이름(또는 더미 호스트 이름인 &quot;_&quot; 문자)이 앞에 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;server_name website.com &quot;&quot;;
sever_name _ &quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MIME 타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;http {
    include       mime.types;
}&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;엔진엑스는 MIME 타입을 구성하는 데 유용한 두 가지 지시어 types와 default_type을 제공합니다. 이 둘은 문서의 기본 MIME 타입을 정의하며, 응답에 포함돼 보내질 Content-Type HTTP 헤더에 영향을 줍니다. types는 MIME 타입과 파일 확장자의 상관관계를 맺는데 쓰이며 어떤 파일을 제공할 때 파일 확장자를 확인해서 MIME 타입을 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;http {
    #log_format  main  '$remote_addr - $remote_user [$time_local] &quot;$request&quot; '
    #                  '$status $body_bytes_sent &quot;$http_referer&quot; '
    #                  '&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;';

  server {
        access_log  logs/host.access.log  combined;
  }    
}&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;로그 파일을 쓸 경로를 지정할 수 있습니다. nginx에서 로그 format으로 제공하는 combined format을 사용할 수도 있고 정의해서 사용할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;경로와 문서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 웹사이트가 제공할 문서를 구성하는 지시어로, 최상위 문서 위치, 사이트 색인, 오류 페이지 같은 것입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;http {
    server {
        server_name localhost;
        root /home/website.com/html;
        location /admin/ {
            alias /var/www/locked/;
        }
    }
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;root
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방문자에게 제공하고자 하는 파일을 담고 있는 최상위 문서 위치를 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;alias
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;location 블록 안에서만 사용&lt;/li&gt;
&lt;li&gt;특정 요청에서 별도의 경로 문서를 읽도록 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
    error_page code1 [code2...] [=대체코드] [@block | URI]
    # ex
    error_page 404 /not_found.html;
    error_page 400 @notfound; # 지정한 location 블록으로 이동
    error_page 404 =200 /index.html # 404오류인 경유 200으로 바꾸고 index.html로 경로를 돌림
}&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;error_page 지시어는 http 응답 코드에 맞춰 URI를 조작하거나 이 코드를 다른 코드로 대체합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 요청 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;http {
    client_body_buffer_size     16k;  
    client_header_buffer_size   8k;
    large_client_header_buffers 4 8k; 
    client_max_body_size        1m;   
    client_header_timeout       5s;  
    client_body_timeout         5s;  
    send_timeout                5s;   
    keepalive_timeout           10s;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 요청에 대한 제한을 정의할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;client_body_buffer_size
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST 등의 요청에 포함되는 body에 대한 버퍼 크기 (default 16k)&lt;/li&gt;
&lt;li&gt;요청의 크기가 너무 크면 본문이나 그 일부가 디스크에 저장&lt;/li&gt;
&lt;li&gt;client_body_in_file_only 지시어가 활성화되면 요청 본문의 크기에 관계없이 언제나 디스크의 파일로 저장됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;client_header_buffer_size
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 헤더에 대한 버퍼 크기 (default 1k)로 토큰 등과 같은 값을 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;large_client_header_buffers
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 보낸 헤더가 옵션의 크기나 갯수를 초과할 때 400이 발생&lt;/li&gt;
&lt;li&gt;URI가 버퍼 하나의 크기보다 크다면 414 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;client_max_body_size
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리퀘스트 body 사이즈에 대한 최대 크기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;client_header_timeout
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 요청 후 응답의 헤더를 보내기까지 기다리는 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;client_body_timeout
&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;기간 초과 시 408 오류를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;send_timeout
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공한 요청 사이 대기 시간&lt;/li&gt;
&lt;li&gt;지정된 시간이 지난 후에 엔진엑스가 비활성 상태의 연결을 닫는다.&lt;/li&gt;
&lt;li&gt;클라이언트가 데이터 전송을 중단하는 순간부터 연결은 비활성 상태가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gzip&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;gzip            on;
gzip_comp_level 9;
gzip_min_length 1000;
gzip_proxied    expired no-cache no-store private auth;
gzip_types      text/plain application/json application/javascript application/x-javascript text/xml text/css application/xml;&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;클라이언트에 전송하기 전에 gzip 알고리즘으로 응답의 본문을 압축할 수 있게 해 줍니다.&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;gzip_comp_level
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;압축 정도&lt;/li&gt;
&lt;li&gt;1(압축률은 낮지만 빠름, 기본값) ~ 9(압축이 많이 되지만 느리다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;gzip_min_length
&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;기본값 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;gzip_proxied
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시에서 받은 응답의 본문을 Gzip으로 압축할지 여부를 결정&lt;/li&gt;
&lt;li&gt;off/any : 모든 요청을 압축할지 여부 결정&lt;/li&gt;
&lt;li&gt;expired : expires 헤더가 캐싱을 하지 않게 돼 있다면 압축을 활성화&lt;/li&gt;
&lt;li&gt;no-cache/no-store/private : cache-control 헤더가 no-cache나 nostore, private으로 설정됐으면 압축을 활성화&lt;/li&gt;
&lt;li&gt;no_last_modified : last-modified 헤더가 없는 경우에 압축을 활성화&lt;/li&gt;
&lt;li&gt;no_etag : ETag 헤더가 없을 경우 압축을 활성화&lt;/li&gt;
&lt;li&gt;auth: Authorization 헤더가 있으면 압축을 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;gzip_types
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 MIME 타입인 text/html 외의 다른 타입에 압축을 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;charset&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;http {
  charset utf-8;  
}&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;응답 content-type 헤더에 특정 인코딩을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;map&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;http {
    map $uri $variable {
      /page.html 0;
      /contact.html 1;
      default 0;
  }
  rewrite % /index.php?page=$variable;
}&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;한 변수의 값에 따라 그에 대응하는 값을 새 변수에 할당합니다. uri 값이 page.html이면 variable 변수의 값은 0이 되는 형식입니다. &lt;b&gt;&lt;code&gt;~(대소문자 구분)&lt;/code&gt;&lt;/b&gt; 또는 &lt;b&gt;&lt;code&gt;~*(대소문자 구분 안함)&lt;/code&gt;&lt;/b&gt;으로 시작하는 패턴 형태로 정규식도 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;map $http_referer $ref {
    ~google &quot;Google&quot;;
    ~* yahho &quot;yahho&quot;;
    \~bing &quot;bing&quot;; # 앞에 \이 있어 정규식이 아님
    default $http_referer; # 변수 사용
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;disable_symlinks&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심볼릭 링크를 웹으로 제공해야 할 때 다루는 방법을 제어합니다. default는 off로 심볼릭 링크가 허용되며, nginx는 링크가 가리키는 파일을 찾습니다. 다음 값 중 하나를 사용해서 특정 조건에 심볼릭 링크가 가리키는 파일을 따라가며 찾지 않도록 할 수 있습니다.&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;on : 요청 URI의 특정 부분이 심볼릭 링크라면 이 접근은 거부되고 403을 반환&lt;/li&gt;
&lt;li&gt;if_not_owner : on 과 비슷하지만 링크와 링크가 가리키는 대상의 소유자가 서로 다를 때 접근 거부&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;location 블록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;server {
    location /admin/ {

    }
}

location [=|~|~*|@] 패턴 { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴터 이름 대신 패턴을 사용해서 처리할 수 있습니다. 패턴 앞쪽에 생략 가능한 인자는 위치 조정 부호라고 부릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조정 부호 생략&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;location /abcd&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;URI가 지정된 패턴으로 시작해야 한다.&lt;/li&gt;
&lt;li&gt;정규식은 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;운영체제가 대소문자를 구분하는 파일 시스템을 사용할 때에만 대소문자를 구분한다.&lt;/li&gt;
&lt;li&gt;querystring은 상관없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;=&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;location = /abcd&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;지정된 패턴과 정확히 일치해야 한다.&lt;/li&gt;
&lt;li&gt;정규식은 사용할 수 없고 단순한 문자열이어야 한다.&lt;/li&gt;
&lt;li&gt;운영체제가 대소문자를 구분하는 파일 시스템을 사용할 때에만 대소문자를 구분한다.&lt;/li&gt;
&lt;li&gt;querystring은 상관없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;~&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# /로 시작하고 d로 끝난다.
location ~ ^/abcd$&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;지정된 정규식에 일치하는지 비교하면서 대소문자를 구분한다.&lt;/li&gt;
&lt;li&gt;querystring은 상관없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;~*&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;location ~* ^/abcd$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식에 일치하는지 비교하면서 대소문자를 구분하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;^~&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조정 부호가 생략된 경우와 비슷하게 동작한다.&lt;/li&gt;
&lt;li&gt;지정된 패턴으로 시작해야 한다.&lt;/li&gt;
&lt;li&gt;패턴이 일치하면 다른 패턴을 찾지 않는다는 것이 차이점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름이 지정된 location 블록을 정의한다.&lt;/li&gt;
&lt;li&gt;외부 클라이언트는 이 블록에 직접 접근할 수 없고 try_files나 error_page 같은 다른 지시어에 의해 생성된 내부 요청만 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;location 탐색 순서와 우선순위&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;location 블록의 순서와 관계없이 특정 순서로 일치하는 패턴을 탐색한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;location /doc{}
location ~* ^/document$ {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&lt;/code&gt;&lt;code style=&quot;letter-spacing: 0px;&quot;&gt;http://website.com/document&lt;/code&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 로 요청을 보낼 때 &lt;/span&gt;&lt;code style=&quot;letter-spacing: 0px;&quot;&gt;~*&lt;/code&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;가 조정 부호가 우선순위가 높아서 두 번째 블록이 적용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;location /document {}
location ~* ^/document$ {}&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;code&gt;&lt;/code&gt;&lt;code style=&quot;letter-spacing: 0px;&quot;&gt;http://website.com/document&lt;/code&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로 요청을 보내면 첫 번째 블록을 적용합니다. 결과적으로 엔진엑스는 정규식보다 구체적인 문자열을 우선시한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;location ^~ /doc {}
location ~* ^/document$ {}&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;code&gt;http://website.com/document&lt;/code&gt;로 요청을 보내면 첫 번째 블록을 적용합니다. &lt;code&gt;^~&lt;/code&gt;가 &lt;code&gt;~*&lt;/code&gt;보다 우선됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;resolver
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔진엑스가 호스트 이름으로 ip 주소를 찾거나 그 반대 작업을 할 때 사용할 DNS 서버를 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;resolver_timeout
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트 이름 ip 변환 요청의 제한시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;server_tokens
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔진엑스가 실행되는 버전 정보를 클라이언트에게 알릴지 여부를 정의
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;on : 정보를 제공(기본)&lt;/li&gt;
&lt;li&gt;off : 서버 헤더에는 엔진엑스를 쓴다는 사실만 기록&lt;/li&gt;
&lt;li&gt;build : 컴파일 시 --build 스위치에 지정한 값이 노출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;underscores_in_headers
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 정의 http 헤더 이름에 밑줄 부호를 허용할지 여부&lt;/li&gt;
&lt;li&gt;기본은 off&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_headers_hash_bucket_size
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 헤더의 해시 테이블용 버킷 크기를 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_set header
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뒷단 서버로 전송될 헤더 값을 다시 정의할 수 있게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_http_version
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 뒷단과 통신하는 데 쓰일 HTTP 버전을 설정한다.&lt;/li&gt;
&lt;li&gt;기본값은 1.0인데 연결 유지해서 재사용하려면 1.1로 설정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_cookie_domain, proxy_cookie_path
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키의 도메인이나 경로 속성을 실시작으로 조작하게 한다.&lt;/li&gt;
&lt;li&gt;대소문자를 따지지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재작성 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 요청&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔진엑스에서는 외부 요청과 내부 요청을 구분합니다. 외부 요청은 클라이언트에서 직접 온 요청을 의미하고 이는 location 블록에 매칭됩니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;server {
    location = /document.html {
        deny all;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;letter-spacing: 0px;&quot;&gt;http://website.com/document.html&lt;/code&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로 들어오는 클라이언트 지시어는 이 location 블록에 정의대로 직접 처리되지만 이와 반대로 내부 요청은 엔진엑스에서 특수한 지시어에 의해 발생합니다. 기본 엔진엑스 모듈에서 제공되는 지시어에는 내부 요청을 발생시키는 지시어가 여럿인데, error_page, index, rewrite, try_files 외에 첨가 모듈의 add_before_body, add_after_body, ssl 명령 등이 있습니다. 내부 요청은 2가지 유형이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 경로 재설정
&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;원래 URI가 바뀌기 때문에 이 요청은 다른 location 블록에 매칭되어 다른 설정이 적용됩니다.&lt;/li&gt;
&lt;li&gt;내부 요청이 쓰이는 가장 일반적인 경우는 rewrite 지시어가 사용되는 경우로 이 지시어는 요청 URI를 재작성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;부가 요청
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원래 요청을 보완할 내용을 생성하고자 내부에서 추가로 새로 만들어지는 요청이 있습니다.&lt;/li&gt;
&lt;li&gt;단순한 예로 첨가 모듈이 있고 add_after_body 지시어는 원래 URI 후에 특정 URI가 처리되게 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;error_page 지시어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;error_page는 특정 오류 코드가 발생했을 때 서버의 행위를 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;server {
    error_page 403 /errors/forbidden.html;
}&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;클라이언트가 오류 중 하나를 일으키는 URI에 접근하려고 할 때 엔진엑스는 오류 코드에 연관된 페이지를 제공합니다. 사실 클라이언트에게 오류 페이지를 전송할 뿐 아니라 URI에 따라 새로운 요청을 발생시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;server {
    error_page 404 /errors/404.html;
    location /erros/ {
        internal;
    }
}&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;클라이언트가 존재하지 않는 문서를 읽으려 할 때 404 오류를 수신합니다. 위에서는 error_page 지시어를 사용해 404 오류가 발생하면 내부적으로 /errors/404.html로 경로가 재설정되도록 지정되어 있습니다. 따라서 엔진엑스는 /errors/404.html로 새로운 요청을 생성합니다. 이 URI는 location 블록의 /errors/에 대응하게 되고 해당 구성이 적용됩니다. location 블록 내부에 &lt;code&gt;internal&lt;/code&gt;지시어로 인해 해당 블록은 클라이언트가 /errors/ 디렉터리에 접근하지 못하도록 차단되고 내부 요청만 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;error_page 지시어가 다른 위치로 경로를 재설정하는 방식과 비슷하게 rewrite 지시어로 URI를 재작성하면 내부 경로 재설정이 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;server {
    location /storage/ {
        internal;
    }
    location /documents/ {
        rewrite ^/documents/(.*)$ /storage/$1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;http://website.com/documents/file.txt&lt;/code&gt;로 들어오는 클라이언트 요청은 처음에는 두 번째 location 블록에서 처리되지만 rewrite를 통해 storage로 변환되면서 요청 처리과정을 처음부터 다시 시작하여 location 첫 번째 블록으로 들어가게 됩니다. 그리고 해당 location은 internal이 명시되어 있으므로 내부 통신만 허용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건부 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
    if ($request_method = POST) {

    }
}&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;if 조건부 구조로 어떤 구성을 특정 조건에서만 적용하게 할 수 있습니다. if 지시어를 통해 location을 대체할 수도 있는데 location을 사용하는 이유는 location 블록에는 대부분의 지시어를 사용할 수 있기 때문입니다. 보통 if 블록 안에는 재작성 모듈의 지시어만 넣는 것이 권장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;접근 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;allow와 deny가 접근 모듈을 통해 제공됩니다. 이 지시어는 특정 ip 주소나 ip 주소 범위에서 자원에 접근하도록 허가하거나 거절할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;location {
    allow 127.0.0.1; # 로컬 ip 주소를 허용
    allow unix:; # 유닉스 도메인 소켓을 허용
    deny all; # 모든 ip 주소 차단
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;규칙들이 위에서 아래로 처리된다는 점을 주의해야 한다.&lt;/code&gt;첫 명령이 deny all이면 그 뒤에 따르는 모든 allow 예외 조건이 아무런 효력을 발휘하지 못합니다. 또한, allow all로 시작하면 그 후에 적힌 모든 deny 지시어는 무효가 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;auth_request 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부가 요청의 결과에 따라 자원 접근을 허락할지 거부할지 결정합니다. 엔진엑스는 auth_request 지시어에 지정한 URI를 호출해서 이런 부가 요청이 2XX 응답 코드를 반환하면 접근을 허용합니다. 부가 요청이 401이나 403 상태 코드를 반환하면 접근이 거부되고 엔진엑스는 해당 응답 코드를 클라이언트에게 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;location /downloads/ {
    # 스크립트가 200 상태를 반환하면 자료를 다운로드를 허용한다.
    auth_request /something/request;
}&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;이 모듈을 auth_request_set 지시어를 통해 부가 요청이 수행된 후에 변수 값을 설정할 수 있게 합니다. 부가 요청에서 기인하는 &lt;code&gt;$upstream_http_server&lt;/code&gt;나 다른 서버 응답의 HTTP 헤더 값을 &lt;code&gt;$upstream_http_*&lt;/code&gt; 형태의 변수로 삽입할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;location /downloads/ {
    auth_reqeust /authorization.php;

    # 인증이 허용됐다고 가정하고, 부가 요청 응답 헤더에서 파일명을 취해 경로를 재설정한다.
    auth_request_set $filename &quot;${upstream_http_x_filename}.zip&quot;;
    rewrite ^ /documents/$filename;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현황 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성 연결 횟수, 처리된 총 요청 횟수 등 서버의 현재 상태에 대한 정보를 제공합니다. 모듈을 활성화하려면 location 블록에 stub_status 지시어를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;lcocation = /nginx_status {
    stub_status on;
    allow 127.0.0.1; # 정보를 외부에 노출하지 않음
    deny all;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 location 블록에 해당하는 요청은 상태 페이지를 얻게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엔진엑스 프록시 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;proxy_pass http://호스트명:포트
proxy_pass http://$server_name:8080;

upstream backend {
    server 127.0.0.1:8080;
    server 127.0.0.1:8081;
}
location ~* \.php$ {
    proxy_pass http://backend;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;proxy_pass
&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;upstream을 사용할 수도 있다.&lt;/li&gt;
&lt;li&gt;변수를 사용할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_hide_header
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 엔진엑스는 뒷단 서버에서 받아 클라이언트에게 돌려줄 응답을 준비하기 때문에 Date, Server, X-pad, X-Accel-* 같은 몇 가지 헤더를 무시합니다.&lt;/li&gt;
&lt;li&gt;이 지시어로 클라이언트로 전달하지 않고 숨길 헤더를 추가로 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_redirect
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뒷단 서버에서 유발된 경로를 재설정으로 Location HTTP 헤더 속 URL을 재작성한다.&lt;/li&gt;
&lt;li&gt;off : 경로 재설정 그대로 전달한다.&lt;/li&gt;
&lt;li&gt;default : proxy_pass 지시어의 값을 호스트 이름으로 사용하고 현재 경로의 문서를 추가한다.&lt;/li&gt;
&lt;li&gt;구성 파일이 순차적으로 해석되기 때문에 proxy_redirect 지시어는 proxy_pass 지시어 다음에 들어가야 한다.&lt;/li&gt;
&lt;li&gt;URL : URL의 일부를 다른 값으로 대체한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐시, 버퍼링, 임시 파일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상적으로는 가능한 뒷단 서버로 전달되는 요청의 수를 줄여야 합니다. 다음 지시어는 캐시 시스템을 구축할 때는 물론 버퍼링 제어 옵션과 엔진엑스가 임시 파일을 다루는 방법을 제어하는데 도움 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;proxy_buffering
&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;on으로 설정 시 엔진엑스는 버퍼가 제공하는 메모리 공간을 사용해서 응답 데이터를 메모리에 저장한다.&lt;/li&gt;
&lt;li&gt;버퍼가 가득 차면 응답 데이터는 임시 파일로 저장될 것이다.&lt;/li&gt;
&lt;li&gt;off면 응답은 그대로 클라이언트에게 전달된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_max_temp_file_size
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0으로 설정 시 프록시 전달에 적합한 요청에 임시 파일을 사용하지 않게 된다.&lt;/li&gt;
&lt;li&gt;임시 파일을 쓰고 싶다면 최대 파일 크기를 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한계치, 시간 제약, 오류&lt;/h2&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;proxy_read_timeout
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뒷단 서버에서 데이터를 읽는 제한시간을 정한다.&lt;/li&gt;
&lt;li&gt;이 시간 제약은 전체 응답 지연이 아닌 읽는 두 작업 사이에 적용된다.&lt;/li&gt;
&lt;li&gt;기본 60s&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_ignore_client_abort
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;on으로 설정하면 클라이언트가 요청을 취소했더라도 엔진엑스는 프록시 요청을 계속 처리한다.&lt;/li&gt;
&lt;li&gt;off인 경우 엔진엑스도 뒷단 서버로 보내는 요청을 취소한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;nginx 설정 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러번 사용될 수 있는 설정은 각각의 location 블록에서 재사용할 수 있도록 별도의 파일로 분리하는 것이 좋습니다. proxy.conf 파일을 만들고 location 블록마다 include 옵션으로 불러와서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;proxy.conf&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;proxy_set_header Host              $host; 
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host  $host;
proxy_set_header X-Real-IP         $remote_addr;

# 외부 Analytics를 위한 header setting
add_header Referrer-Policy unsafe-url;

proxy_set_header Connection        &quot;&quot;;
proxy_http_version        1.1;

proxy_redirect            off;
proxy_read_timeout        60s;
proxy_ignore_client_abort on;

# 두 옵션은 고려가 필요
proxy_buffering           on;
proxy_max_temp_file_size  0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;proxy_set_header Host $host
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뒷단 서버로 전달되는 요청의 host HTTP 헤더는 기본값으로 구성 파일에 지정한 프록시의 호스트명입니다.&lt;/li&gt;
&lt;li&gt;이 설정은 엔진엑스가 클라이언트 요청의 원래 host 값을 대신 사용하도록 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_set_header X-Real-IP $remote_addr
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뒷단 서버가 엔진엑스에서 오는 요청을 수신하는 이상, 통신하는 IP 주소가 클라이언트 것이 아닙니다.&lt;/li&gt;
&lt;li&gt;이 설정을 사용해서 클라이언트의 실제 ip주소를 x-real-ip라는 새로운 헤더에 담아서 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_set_header X-forwared-For $proxy_add_X_forwared_for
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X-Real_IP와 비슷하지만, 클라이언트가 이미 스스로 프록시를 사용하고 있다면 클라이언트의 실제 IP주소는 X-Forwared_For 라는 요청 헤더에 들어있을 것입니다.&lt;/li&gt;
&lt;li&gt;통신에 사용하는 소켓과 (프록시 뒤에 있는)클라이언트의 원래 IP 주소 모두 뒷단 서버에 전달하는데 $proxy_add_x_forwared_for을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;X-Forwarded-Proto
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;client에서는 nginx로 https 요청을 보내지만 프록시는 서버로 http로 요청을 보내기 때문에 해당 해더로 client 요청 schema가 https인지 http 인지 정보를 넘겨줄 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Connection, proxy_http_version
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx는 upstream 서버로 proxy를 할 때 HTTP 버전을 1.0으로, Connection 헤더를 close로 변경해서 전달합니다.&lt;/li&gt;
&lt;li&gt;connection을 유지하기 위해서는 HTTP 버전은 1.1로, connection 헤더는 없애줘야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_ignore_client_abort
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;on으로 설정 시, 클라이언트가 요청을 중지시켜도 계속해서 프록시 요청을 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_redirect
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;off로 설정 시, 리다이렉션에 대해 Location 헤더를 재작성하는 기능을 off 시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_read_timeout
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 작업용 제한 시간을 정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_buffering
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nginx를 리버스 프록시로 사용할 때, 클라이언트와 서버 사이의 데이터 전송 속도 차이를 극복하기 위해 사용하는 옵션&lt;/li&gt;
&lt;li&gt;on
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx가 백엔드 서버에서 전체 응답을 수신하고 버퍼링 한 후 클라이언트에 전송&lt;/li&gt;
&lt;li&gt;백엔드 서버와의 연결이 빨리 종료되어 서버의 리소스를 절약 가능&lt;/li&gt;
&lt;li&gt;클라이언트가 데이터를 천천히 받더라도 Nginx가 모든 데이터를 빠르게 받기 때문에 전체 성능이 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;off
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx가 백엔드 서버의 응답을 클라이언트로 직접 전달&lt;/li&gt;
&lt;li&gt;백엔드 서버와 클라이언트 사이의 속도 차이로 인해 백엔드 서버의 리소스가 더 오래 사용될 수 있음&lt;/li&gt;
&lt;li&gt;실시간 스트리밍과 같이 지연 시간이 중요한 경우, 이 옵션을 사용하여 데이터를 실시간으로 전송 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;고려 사항
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드 서버의 리소스 절약이 중요 : on 옵션 사용하여 백엔드 서버와의 연결을 빨리 종료할 수 있도록&lt;/li&gt;
&lt;li&gt;지연 시간이 중요한 실시간 앱의 경우 : off를 사용하여 nginx가 데이터를 클라이언트에게 직접 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proxy_max_temp_file_size
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering&quot;&gt;http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;buffer가 부족하면 파일에 일부를 디스크의 임시 파일에 저장하게 되는데 0으로 설정하면 파일에 쓰지 않겠다는 의미입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nginx.conf 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# k8s 클러스터의 물리 장비의 cpu 개수로 판단해서 auto를 사용하지 않고 cpu 코어 개수로 명시
worker_processes 2;

pid /home/backtony/logs/nginx/nginx.pid;

events {
    # reverse proxy의 경우 worker_processes * worker_connections / 4 값은 ulimit -n결과값(open files) 보다 작아야 한다.
    # 총 커넥션은 worker_processes * worker_connections 수가 됨을 유의해서 설정한다. 웹서버 장비에서 `ulimit -n`로 한계를 확인해본다.
    # 보통 위와 같이 계산하기보다는 1024면 충분하다.
    worker_connections 1024;
    # epoll을 사용하면 수천 개의 연결을 제공해야 할 때 CPU 사용량이 줄어든다.
    use epoll;
}

http {
    include      mime.types;
    default_type application/octet-stream;

    charset utf-8;

    client_body_buffer_size     16k;  # POST등의 요청에 포함되는 body에 대한 버퍼 크기 (default : 16k for x86-64)
    client_header_buffer_size   8k;   # 요청 헤더에 대한 버퍼 크기 (default : 1k) - 토큰이나 그런 값들을 고려해서 설정
    client_max_body_size        1m;   # 리퀘스트 body사이즈에 대한 최대 크기 (default : 1m)
    large_client_header_buffers 4 8k; # 긴 URL 요청으로 들어올 수 있는 헤더의 최대크기. 이를 초과할경우 414 응답
    client_header_timeout       5s;   # 클라이언트 요청 후 응답의 헤더 보내기까지 기다리는 시간
    client_body_timeout         5s;   # 클라이언트 요청 후 응답의 바디 보내기까지 기다리는 시간
    send_timeout                5s;   # 성공한 요청 사이 대기 시간
    keepalive_timeout           10s;

    gzip            on;
    gzip_comp_level 6;
    gzip_min_length 1000;
    gzip_proxied    expired no-cache no-store private auth;
    gzip_types      text/plain application/json application/javascript application/x-javascript text/xml text/css application/xml;

    server_tokens    off;
    disable_symlinks on;

    # 적은 데이터를 real time에 빠르게 보내기 위한 옵션
    # 작은 데이터 패킷 지연 없이 전송
    tcp_nodelay on;

    upstream webapp {
        server 127.0.0.1:8080;
        # 엔진엑스와 백엔드 서버간의 연결 유지하는 기능(클라이언트가 아니라 엔진엑스)
        # 엔진엑스와 백엔드 서버 사이에 여러번 요청을 처리하는 경우에 유용
        # 매번 연결을 새로 만들 필요 없이 기존 연결을 유지한 채로 여러 요청을 처리
        # 즉, 엔진엑스랑 백엔드 서버랑 핸드쉐이킹 작업 없이 커넥션 100개를 유지할 수 있으니 속도를 더 빠르게 할 수 있다.
        keepalive 100;
    }

    proxy_headers_hash_bucket_size 128;

    server {
        listen 80 default_server;

        set $loggable 1;
        if ($http_user_agent ~* &quot;(kube-probe|NGINX-Prometheus-Exporter)&quot;) {
            set $loggable 0;
        }

        # for console!
        access_log /dev/stdout combined if=$loggable;
        error_log /dev/stderr info;

        location / {
            return 444;
        }

        location /http_stub_status {
            # 허용할 ip, cluster ip 목록들 명시
            allow 127.0.0.1/32;
            allow 10.0.0.0/8;
            stub_status;
            deny all;
        }
    }

    server {
        listen 80;
        server_name nginx 도메인;

        set $loggable 1;
        if ($http_user_agent ~* &quot;(kube-probe|NGINX-Prometheus-Exporter)&quot;) {
            set $loggable 0;
        }

        access_log /home/backtony/logs/nginx/access.log combined if=$loggable;
        error_log /home/backtony/logs/nginx/error.log info;

        access_log /dev/stdout combined if=$loggable;
        error_log /dev/stderr info;

        underscores_in_headers on;

        location /http_stub_status {
            allow 127.0.0.1/32;
            allow 10.0.0.0/8;
            stub_status;
            deny all;
        }

        location / {
            proxy_pass http://webapp;
            include /home/backtony/apps/nginx/conf/proxy.conf;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&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;a href=&quot;https://kwonnam.pe.kr/wiki/nginx/performance&quot;&gt;https://kwonnam.pe.kr/wiki/nginx/performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/322&quot;&gt;https://jojoldu.tistory.com/322&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/v0lkan/90fcb83c86918732b894&quot;&gt;https://gist.github.com/v0lkan/90fcb83c86918732b894&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/denji/8359866&quot;&gt;https://gist.github.com/denji/8359866&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nginx.com/blog/performance-tuning-tips-tricks/&quot;&gt;https://www.nginx.com/blog/performance-tuning-tips-tricks/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://thoughts.t37.net/nginx-optimization-understanding-sendfile-tcp-nodelay-and-tcp-nopush-c55cdd276765&quot;&gt;https://thoughts.t37.net/nginx-optimization-understanding-sendfile-tcp-nodelay-and-tcp-nopush-c55cdd276765&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration&quot;&gt;https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>NGINX</category>
      <category>HTTP</category>
      <category>http 모듈</category>
      <category>nginx</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/62</guid>
      <comments>https://backtony.tistory.com/62#entry62comment</comments>
      <pubDate>Sun, 14 Jan 2024 11:18:48 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - 등장 배경과 사용 용도</title>
      <link>https://backtony.tistory.com/61</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;NginX란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NginX는 웹 서버이며, 리버스 프록시, 로드 밸런서, HTTP 캐시로 등으로 쓰일 수 있는 소프트웨어입니다. 요청에 응답하기 위해 이벤트 기반 구조를 채택하여 자원을 효율적으로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;등장 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1031&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v9b64/btsCQssNPL8/3nQHX2aXpb8jkjyKncQT91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v9b64/btsCQssNPL8/3nQHX2aXpb8jkjyKncQT91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v9b64/btsCQssNPL8/3nQHX2aXpb8jkjyKncQT91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv9b64%2FbtsCQssNPL8%2F3nQHX2aXpb8jkjyKncQT91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1031&quot; height=&quot;416&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초의 웹서버 NCSA에 수많은 버그로 인해 이를 보완한 아파치 서버가 등장합니다. 아파치 서버는 요청이 들어오면 커넥션을 형성하기 위해 프로세스를 생성하는 방식으로 새로운 클라이언트 요청이 들어올 때마다 새로운 프로세스를 생성합니다. 프로세스를 만드는 작업은 오래 걸리므로 미리 프로세스를 만들어 놓는 prefork 방식을 사용합니다. 만약 미리 만들어 놓은 프로세스가 모두 할당되었다면 추가로 프로세스를 만들게 됩니다. 이런 구조로 인해 개발자는 다양한 모듈을 만들어 빠르게 기능을 추가할 수 있게 되었고 동적 콘텐츠도 처리할 수 있게 되었습니다. 또한 확장성이 좋다는 장점 때문에 요청을 받고 응답하는 과정을 하나의 서버에서 처리하기도 좋아지면서 아파치가 대세로 자리 잡습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1161&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpPVRU/btsCZiPE2fy/oHRqPJQtHfVuOuiYSZI2i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpPVRU/btsCZiPE2fy/oHRqPJQtHfVuOuiYSZI2i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpPVRU/btsCZiPE2fy/oHRqPJQtHfVuOuiYSZI2i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpPVRU%2FbtsCZiPE2fy%2FoHRqPJQtHfVuOuiYSZI2i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1161&quot; height=&quot;481&quot; data-origin-width=&quot;1161&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파치 서버가 대세가 되었을 당시에는 서버가 처리해야 할 요청량이 그 당시 감당할 수 있을 정도였습니다. 트래픽이 점점 증가하면서 서버에 동시에 연결된 커넥션이 많아졌을 때 더 이상 커넥션을 형성하지 못하는 문제가 발생하게 되었습니다. 이를 C10K라고 하고 커넥션 10000개의 문제라고 합니다. 동시에 연결된 커넥션 수란 요청을 처리하기 위해 서버가 한 시점에 얼마나 많은 클라이언트와 커넥션을 형성하는지를 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 아파치 서버의 구조였습니다. 아파치 서버는 구조상 커넥션이 형성될 때마다 새로운 프로세스를 할당하기 때문에 동시에 처리하는 커넥션이 많아지면 그만큼 형성되는 프로세스가 많아지는 것이고 이는 메모리 부족 현상으로 이어지게 됩니다. 그리고 아파치 서버는 여러 가지 기능을 쉽게 추가할 수 있는 특징으로 인해 특정 프로세스가 차지하는 리소스의 양이 늘어나게 됩니다. 또한, 많은 커넥션에서 요청이 들어오기 시작하면 CPU 코어는 계속해서 프로세스를 바꿔가며 작업하는 컨텍스트 스위칭 비용이 커지게 됩니다. 따라서 수많은 동시 커넥션을 감당하기에는 아파치 서버의 구조가 적합하지 않았던 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh4hOw/btsCTxUFBDs/J8iNVpqEJmsAWHYUN2DCWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh4hOw/btsCTxUFBDs/J8iNVpqEJmsAWHYUN2DCWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh4hOw/btsCTxUFBDs/J8iNVpqEJmsAWHYUN2DCWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh4hOw%2FbtsCTxUFBDs%2FJ8iNVpqEJmsAWHYUN2DCWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1157&quot; height=&quot;481&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조를 보완한 nginx가 등장합니다. 초창기에는 아파치 서버와 함께 사용하기 위해 만들어졌습니다. 아파치 서버의 구조적 한계를 극복하기 위해 아파치 서버 앞에 두면서 기존에 아파치 서버가 감당했던 수많은 동시커넥션을 엔진엑스가 받도록 하는 것입니다. 엔진엑스는 웹서버이기 때문에 정적인 처리는 스스로 했고, 동적 파일 요청을 받았을 때만 아파치 서버와 커넥션을 만들어 처리했습니다. 아파치의 리소스를 커넥션 유지에 쓰지 않고 실제 로직 처리에 사용하도록 하는 구조로 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6yX7w/btsCTYEoKJj/PArXGwCgnWM9EH0fVaw0z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6yX7w/btsCTYEoKJj/PArXGwCgnWM9EH0fVaw0z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6yX7w/btsCTYEoKJj/PArXGwCgnWM9EH0fVaw0z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6yX7w%2FbtsCTYEoKJj%2FPArXGwCgnWM9EH0fVaw0z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;798&quot; height=&quot;482&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔진엑스는 마스터 프로세스에서 설정파일을 읽고 설정에 맞게 워커 프로세스를 생성하고 워커 프로세스는 실제로 일을 하게 됩니다. 워커 프로세스는 클라이언트로부터 요청이 들어오면 커넥션을 생성하고 Keep-Alive 시간만큼 커넥션을 유지하면서 요청을 처리합니다. 그런데 커넥션이 형성되었다고 해서 워커 프로세스가 커넥션을 하나만 담당하진 않습니다. 형성된 커넥션에 아무런 요청이 없으면 새로운 커넥션을 형성하거나 이미 만들어진 다른 커넥션으로부터의 요청을 처리합니다. 엔진엑스에서는 이런 커넥션 형성, 커넥션 제거, 새로운 요청을 처리하는 것을 이벤트라고 부릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8GaBS/btsCTvicCwC/ORYBmTqF6hukBcenKDWUek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8GaBS/btsCTvicCwC/ORYBmTqF6hukBcenKDWUek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8GaBS/btsCTvicCwC/ORYBmTqF6hukBcenKDWUek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8GaBS%2FbtsCTvicCwC%2FORYBmTqF6hukBcenKDWUek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;981&quot; height=&quot;436&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트들은 OS 커널을 통해 큐형식으로 워커 프로세스에게 전달됩니다. 이벤트는 큐에서 워커 프로세스가 처리할 때까지 비동기 방식으로 대기합니다. 워커 프로세스는 하나의 스레드로 이벤트를 꺼내서 처리하게 되면서 쉬지 않고 일하는 장점을 가져가게 됩니다. 아파치 서버 구조와 비교했을 때 요청이 없다면 방치되는 프로세스보다 훨씬 효율적으로 서버자원을 쓰는 셈입니다. 만약 요청 중 하나가 시간이 오래 걸리는 작업이라면 시간이 오래걸리는 작업을 따로 수행하는 쓰레드풀을 만들어 놓고 오래걸리는 작업은 스레드풀에게 이벤트를 위임하고 다음 이벤트를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1111&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/395ep/btsCZj8Uyf4/rOPdFuCi4k2sgaegDDYL80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/395ep/btsCZj8Uyf4/rOPdFuCi4k2sgaegDDYL80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/395ep/btsCZj8Uyf4/rOPdFuCi4k2sgaegDDYL80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F395ep%2FbtsCZj8Uyf4%2FrOPdFuCi4k2sgaegDDYL80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1111&quot; height=&quot;469&quot; data-origin-width=&quot;1111&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 워커 프로세스는 cpu의 코어개수만큼 생성합니다. 이러면 코어가 담당하는 프로세스를 바꾸는 횟수를 대폭 줄일 수 있습니다. 즉, cpu가 컨텍스트 스위칭 사용을 줄일 수 있게 되는 것입니다. 이것이 엔진엑스가 채택한 이벤트 기반 구조로 아파치 서버와 가장 큰 차이점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1UpEI/btsCTwVHTbt/G5VVKfne13XsM3WDsf0yb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1UpEI/btsCTwVHTbt/G5VVKfne13XsM3WDsf0yb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1UpEI/btsCTwVHTbt/G5VVKfne13XsM3WDsf0yb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1UpEI%2FbtsCTwVHTbt%2FG5VVKfne13XsM3WDsf0yb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;875&quot; height=&quot;506&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스를 적게 만드는 구조는 엔진엑스의 설정을 동적으로 바꾸는 것을 가능하게 했습니다. 개발자가 설정파일을 변경하고 엔진엑스에 해당 설정을 적용하면 마스터 프로세스는 그 설정에 맞는 워커 프로세스를 따로 생성하고 기존 워커 프로세스가 더 이상 커넥션을 형성하지 않도록 합니다. 시간이 지나 기존 워커 프로세스가 담당하던 이벤트 처리가 끝나면 해당 프로세스를 종료합니다. 대표적으로 엔진엑스가 동시 커넥션을 관리하는 도중에 뒷단에 서버를 추가하는 상황이 있습니다. 동적으로 설정을 변경할 수 있기 때문에 기존 요청을 처리하면서 설정을 변경하여 뒷단에 서버 추가 설정이 가능하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 배경으로 NginX가 등장하게 되었습니다. APACHE도 동시 커넥션 관련 업데이트를 해오고 있지만 동시 커넥션 관련 지표에서는 NginX가 성능상 우세한 퍼포먼스를 보이고 있습니다. 동시 커넥션이라는 포인트에만 집중한다면 NginX가 우세하지만, 아파치와 NginX가 점유율을 다투는 데는 다 이유가 있습니다. 아파치의 경우 오랜 기간 업데이트로 서버 자체가 다양한 OS에서 안정적이라는 장점이 있지만, NginX는 그렇지 않아서 윈도우에서 제대로 된 성능을 발휘하지 못합니다. 그리고 아파치는 모듈 추가에서 기능을 확장하기 쉽다는 장점과 모듈의 종류도 NginX보다 훨씬 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 용도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔진엑스는 다음과 같은 용도로 많이 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹서버 기능, 로드 밸런서, 캐싱, HSTS, HTTP/2, TCP/UDP 커넥션 부하분산, CORS 처리&lt;/li&gt;
&lt;li&gt;SSL 터미네이션 : 클라이언트와는 https 통신, 서버와는 http 통신하여 서버는 복호화 과정을 담당하지 않는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=6FAwAXXj5N0&amp;amp;list=PLo0ta52hn1uHQ5iQ3hAeRoMUeLJFIeRew&amp;amp;index=2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[10분&amp;nbsp;테코톡]&amp;nbsp;:shushing_face:&amp;nbsp;피케이의&amp;nbsp;Nginx&lt;/a&gt;&lt;/p&gt;</description>
      <category>NGINX</category>
      <category>nginx</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/61</guid>
      <comments>https://backtony.tistory.com/61#entry61comment</comments>
      <pubDate>Wed, 3 Jan 2024 00:10:53 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 8.0 쿼리와 인덱스 처리 방식</title>
      <link>https://backtony.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;order by 처리(using filesort)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬을 처리하는 방법은 인덱스를 이용하는 방법과 쿼리가 실행될 때 Filesort라는 별도의 처리를 이용하는 방법으로 나눌 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 이용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;insert, update, delete 쿼리가 실행될 때 이미 인덱스가 정렬돼 있어서 순서대로 읽기만 하면 되므로 매우 빠름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;insert, update, delete 작업 시 부가적인 인덱스 추가/삭제 작업이 필요하므로 느리다.&lt;/li&gt;
&lt;li&gt;인덱스 때문에 추가 디스크 공간이 필요하고 늘어날수록 innoDB의 버퍼 풀을 위한 메모리가 많이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;filesort 이용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 생성하지 않아도 되므로 인덱스를 이용할 때의 단점이 장점으로 바뀐다.&lt;/li&gt;
&lt;li&gt;정렬해야 할 레코드가 많지 않으면 메모리에서 filesort가 처리되므로 충분히 빠르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정렬 작업이 쿼리 실행 시 처리되므로 레코드 대상 건수가 많아질수록 느려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 이유로 모든 경우에 대해서 정렬을 인덱스 튜닝하기는 불가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정렬 기준이 너무 많아서 요건별로 모두 인덱스 생성이 불가능한 경우&lt;/li&gt;
&lt;li&gt;group by의 결과 또는 distinct 같은 처리의 결과를 정렬해야 하는 경우&lt;/li&gt;
&lt;li&gt;union의 결과와 같이 임시 테이블의 결과를 다시 정렬해야하는 경우&lt;/li&gt;
&lt;li&gt;랜덤하게 결과 레코드를 가져와야하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 잘 활용하는지 아닌지는 explain을 떠보면 extra 컬럼에 using filesort 메시지가 표시되는지 여부로 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리에 order by를 사용하면 반드시 3가지 처리중 한 가지가 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;정렬 처리 방법&lt;/th&gt;
&lt;th&gt;실행 계획의 extra 컬럼 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인덱스를 사용한 정렬&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조인에서 드라이빙(기준) 테이블만 정렬&lt;/td&gt;
&lt;td&gt;using filesort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조인에서 조인 결과를 임시 테이블로 저장 후 정렬&lt;/td&gt;
&lt;td&gt;using temporary; using filesort&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 이용할 수 있다면 별도의 표기가 없고 사용할 수 없다면 where 조건에 일치하는 레코드를 검색해 정렬 버퍼에 저장하면서 정렬을 처리(file sort)한다. mysql 옵티마이저는 정렬 대상 레코드를 최소화하기 위해 다음 2가지 방법 중 선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인의 드라이빙 테이블(기준)만 정렬한 다음 조인을 수행&lt;/li&gt;
&lt;li&gt;조인이 끝나고 일치하는 레코드를 모두 가져온 후 정렬을 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 조인이 수행되면 레코드 건수와 레코드의 크기가 배수로 불어나기 때문에 가능하다면 드라이빙 테이블(기준)만 정렬한 다음 조인을 수행하는 방법이 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스를 이용한 정렬&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 이용한 정렬을 위해서는 반드시 ORDER BY 절에 명시된 컬럼이 드라이빙 테이블(기준 테이블)에 속하고 ORDER BY의 순서대로 생성된 인덱스가 있어야 한다. 또한 WHERE 절에 드라이빙 테이블(기준 테이블)의 컬럼에 대한 조건이 있다면 그 조건과 ORDER BY는 같은 인덱스를 사용할 수 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employee e, salaries s
WHERE s.emp_no = e.emp_no
    AND e.emp_no BETWEEN 100 AND 200
ORDER BY e.emp_no&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조인의 드라이빙 테이블(기준 테이블)만 정렬&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 조인이 되면 데이터가 불어나기 때문에 드라이빙 테이블만 먼저 정렬하고 조인을 실행하는 것이 차선책이 된다. 이 방법으로 정렬이 처리되려면 드라이빙 테이블의 컬럼만으로 ORDER BY 절을 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employee e, salaries s
WHERE s.emp_no = e.emp_no
    AND e.emp_no BETWEEN 100 AND 200
ORDER BY e.last_name&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where절이 다음 조건을 갖추고 있어서 옵티아미저는 employee 테이블을 드라이빙 테이블로 선택할 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where 절의 검색 조건은 employee의 프라이머리 키를 이용해 검색하면 작업을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;드리븐 테이블(salaries)의 조인 컬럼인 emp_no 컬럼에 인덱스가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색은 인덱스로 처리할 수 있지만 order by 절에 명시된 컬럼이 프라이머리 키와 전혀 연관이 없으므로 인덱스를 이용한 정렬이 불가능하다. 그런데 정렬 컬럼이 드라이빙 테이블에 포함된 컬럼이므로 옵티마이저는 드라이빙 테이블만 검색해서 정렬을 먼저 수행하고 그 결과를 salaries 테이블과 조인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임시 테이블을 이용한 정렬&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인의 드라이빙 테이블 사용을 제외한 2개 이상의 테이블 조인에서는 항상 조인 결과를 임시 테이블에 저장하고 그 결과를 다시 정렬하는 과정을 거친다. 이 방법은 3가지 방법중에 가장 느린 방법이다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employee e, salaries s
WHERE s.emp_no = e.emp_no
    AND e.emp_no BETWEEN 100 AND 200
ORDER BY s.salary&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;order by 정렬 컬럼이 드리븐 테이블(salaries)에 있는 컬럼이다. 즉, 정렬이 수행되기 전에 salaries 테이블을 읽어야 하므로 조인된 데이터를 가지고 정렬할 수밖에 없다. 따라서 explain을 떠보면 filesort, using temporary가 표기된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GROUP BY 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY 절이 있는 쿼리에서는 Having 절을 사용할 수 있는데 이는 GROUP BY 결과에 대해 필터링할 수 있는 역할을 수행한다. &lt;code&gt;GROUP BY에 사용된 조건이 인덱스가 사용되지 않았다면 Having절의 조건을 위해 인덱스를 생성하거나 다른 방법을 고민하는 것은 효과가 미미하다.&lt;/code&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;GROUP BY 절도 인덱스를 사용하는 경우와 그렇지 못한 경우로 나뉜다. 인덱스를 이용할 때는 인덱스를 차례대로 읽는 인덱스 스캔 방법과 인덱스를 건너뛰면서 읽는 루스 인덱스 스캔이라는 방법으로 나뉜다. 그리고 인덱스를 사용하지 못하는 쿼리에서 GROUP BY 작업은 임시 테이블을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 스캔을 이용하는 GROUP BY(타이트 인덱스 스캔)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORDER BY의 경우와 마찬가지로 조인의 드라이빙 테이블에 속한 컬럼만 이용해서 그루핑할 때 GROUP BY 컬럼으로 이미 인덱스가 있다면 그 인덱스를 차례로 읽으면서 그루핑 작업을 수행하고 그 결과로 조인을 처리한다. GROUP BY가 인덱스를 사용해서 처리된다 하더라도 그룹함수(aggregation function) 등의 그룹값을 처리해서 임시 테이블이 필요할 때도 있다. GROUP BY가 인덱스를 통해 처리되는 쿼리는 이미 정렬된 인덱스를 읽는 것이므로 쿼리 실행 시점에 추가적인 정렬 작업이나 내부 임시 테이블은 필요하지 않다. 이러한 그루핑 방식을 사용하는 쿼리의 실행 계획에서는 Extra 컬럼에 별도로 GROUP BY 관련 코멘트인 (using index for group-by)나 임시 테이블 사용 또는 정렬 관련 코멘트(using temporary, using filesort)가 표시되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;루스 인덱스 스캔을 이용하는 GROUP BY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루스 인덱스 스캔 방식은 인덱스의 레코드를 건너뛰면서 필요한 부분만 읽어서 가져오는 것을 의미하는데, 옵티마이저가 루스 인덱스 스캔을 사용할 때는 실행 계획의 Extra 컬럼에 &lt;code&gt;Using index for group-by 코멘트가 표시된다.&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;select emp_no
from salaries
where from_date = '1984-03-01'
group by emp_no&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;salaries 테이블의 인덱스는 (emp_no, from_date) 로 생성되어있으므로 where 조건은 인덱스 레인지 스캔 접근 방식으로는 이용할 수 없다. 하지만 explain을 떠보면 인덱스 레인지 스캔을 이용했으며 extra 컬럼 메시지에 인덱스 사용이 표기된다.(using where; using index for group-by) 이 방식은 단일 테이블 group by에서만 사용할 수 있고 prefix index(컬럼값의 앞쪽 일부만으로 생성된 인덱스)는 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임시 테이블을 사용하는 GROUP BY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 기준 컬럼이 드라이빙에 있는 드리븐에 있든 관계없이 인덱스를 전혀 사용하지 못할때 사용된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;select e.last_name, avg(s.salary)
from employees e, salaries s
where s.emp_no = e.emp_no
group by e.last_name&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 떠보면 using temporary가 표기된다. 테이블 풀 스캔이 아니라 인덱스를 전혀 사용하지 못하기 때문이다. 그런데 using filesort는 표기되지 않고 useing temporary만 표기된다. 8.0 이전버전에는 group by가 사용된 쿼리는 그루핑되는 컬럼을 기준으로 묵시적인 정렬까지 함께 수행됐다. 그래서 이전에는 group by는 있지만 order by 절이 없는 쿼리에 대해서는 기본적으로 그루핑 컬럼인 last_name에 대해서 정렬이 수행된 상태로 결과값을 반환했다. 하지만 8.0부터는 묵시적인 정렬은 실행되지 않고 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유 때문에 8.0 이전에는 group by후 정렬이 필요하지 않은 경우, &lt;code&gt;order by null&lt;/code&gt; 사용이 권장되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql 8.0에서는 group by가 필요한 경우 내부적으로 group by 절의 컬럼으로 구성된 유니크 인덱스를 가진 임시 테이블을 만들어서 중복 제거와 집합 함수 연산을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create temporary table ... (
    last_name varchar(16),
    salary int,
    unique index ux_lastname (last_name)
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Distinct 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;distinct 처리는 인덱스를 사용하지 못할 때는 항상 임시테이블이 필요하다.&lt;/b&gt; 하지만 실행 계획에서는 using temporary가 출력되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;select distinct&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select distinct first_name, last_name from employees;
select distinct (first_name), last_name from employees;&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;distinct키워드는 뒤에 () 괄호는 실행시 제거된다&lt;/b&gt;. 즉, distinct는 first_name만 유니크한게 아니고 (first_name, last_name) 조합 전체가 유니크한 레코드를 가져온다. 이에 대한 예외는 집합 함수와 함께 사용되는 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;집합 함수와 함께 사용된 distinct&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count, min, max 같은 집합 함수 내에서 distinct 키워드가 사용될 수 있는데 일반적으로 select distinct와는 다른 형태로 해석된다. select 쿼리에서 distinct는 조회 컬럼 모든 조합이 유니크한 것들만 가져오지만 집합 함수 내에서는 사용된 distinct의 함수 인자로 전달된 컬럼 값이 유니크한 것들만 가져온다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select count(distinct s.salary)
from employees e, salaries s
where e.emp_no=s.emp_no
and e.emp_no between 100 and 200&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;쿼리는 내부적으로 count를 처리하기 위해 임시테이블을 사용하지만 실행 계획에는 표기되지 않는다. 위 쿼리는 조인 결과에서 salary 컬럼의 값만 저장하기 위한 임시테이블을 만들어서 사용한다. 임시 테이블의 salary 컬럼에는 유니크 인덱스가 생성되기 때문에 레코드 건수가 많아진다면 상당히 느려질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select count(distinct s.salary)
    count(distinct e.last_name)
from employees e, salaries s
where e.emp_no=s.emp_no
and e.emp_no between 100 and 200&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;만약 위와 같이 count 쿼리를 추가하면 임시 테이블이 2개가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;임시 테이블이 필요한 쿼리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 패턴은 별도의 데이터 가공이 필요하여 내부 임시 테이블이 생성되는 케이스다. 물론 이 밖에도 인덱스를 사용하지 못할 때는 내부 임시테이블을 생성해야할 때가 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;order by와 group by에 명시된 컬럼이 다른 쿼리&lt;/li&gt;
&lt;li&gt;order by나 group by에 명시된 컬럼이 조인의 순서상 첫 번째 테이블이 아닌 쿼리&lt;/li&gt;
&lt;li&gt;distinct와 order by가 동시에 쿼리에 존재하는 경우&lt;/li&gt;
&lt;li&gt;distinct가 인덱스로 처리되지 못하는 쿼리&lt;/li&gt;
&lt;li&gt;union, union distinct가 사용된 쿼리(select_type 컬럼이 union result인 경우)&lt;/li&gt;
&lt;li&gt;쿼리의 실행 계획에서 select_type이 DERIVED인 쿼리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SELECT 절 실행 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select s.emp_no, COUNT(DISTINCT e.first_name) AS cnt
FROM salaries s
    INNER JOIN employees on e.emp_no=s.emp_no
WHERE s.emp_no IN (10001, 10002)
GROUP BY s.emp_no
HAVING ABG(s.salary) &amp;gt; 1000
ORDER BY AVG(s.salary)
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;FROM 및 조인&lt;/li&gt;
&lt;li&gt;WHERE 절&lt;/li&gt;
&lt;li&gt;GROUP BY&lt;/li&gt;
&lt;li&gt;DISTINCT&lt;/li&gt;
&lt;li&gt;HAVING&lt;/li&gt;
&lt;li&gt;ORDER BY&lt;/li&gt;
&lt;li&gt;LIMIT&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순으로 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 순서와 다르게 예외적으로 다음과 같은 순서로 적용되는 경우가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;드라이빙(주) 테이블 WHERE 절 적용&lt;/li&gt;
&lt;li&gt;ORDER BY 절&lt;/li&gt;
&lt;li&gt;드리븐(대상) 테이블 조인 실행&lt;/li&gt;
&lt;li&gt;LIMIT&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 형태는 주로 GROUP BY 절 없이 ORDER BY만 사용된 쿼리에서 사용될 수 있는 순서다. 위와 같은 두가지 순서가 거의 모든 쿼리에서 적용된다. 이에 벗어나는 쿼리 순서가 있다면 서브쿼리로 작성된 인라인 뷰를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WHERE, GROUP BY, ORDER BY 인덱스 사용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 사용 기본 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하려면 기본적으로 인덱스된 컬럼 값 자체를 변환하지 않고 그대로 사용한다는 조건을 만족해야 한다. 즉, WHERE 절에서 사용할 떄, 가공(곱셈 같은 연산)을 하면 제대로 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;where 절 인덱스 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&lt;b&gt;where 조건절에 나열된 컬럼 순서는 실제 인덱스의 사용 여부와 무관하다.&lt;/b&gt;&lt;/code&gt; where 절에 나열된 컬럼 순서와 인덱스의 컬럼 순서가 다르더라도 옵티마이저가 조건들을 뽑아서 최적화를 수행한다. 8.0이전 버전까지는 하나의 인덱스를 구성하는 각 컬럼의 &lt;code&gt;정렬 순서&lt;/code&gt;가 혼합되어 사용할 수 없었지만 8.0부터는 인덱스를 구성하는 컬럼별로 오름차순, 내림차순 정렬을 혼합해서 생성할 수 있게 개선되었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE ... ADD INDEX hello (col_1 ASC, col_2 DESC)&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;여러 방향으로 정렬을 하고 싶다면 위처럼 인덱스를 만들때, ASC, DESC 옵션을 섞어서 인덱스를 만들어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUP BY 절 인덱스 사용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;&lt;b&gt;WHERE절과 달리 group by 절에 명시된 컬럼이 인덱스 컬럼의 순서와 위치가 같아야 한다.&lt;/b&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;인덱스를 구성하는 컬럼 중 뒤쪽에 있는 컬럼은 GROUP BY 절에 명시되어있지 않아도 인덱스를 사용할 수 있지만 앞쪽에 있는 컬럼이 GROUP BY 절에 명시되지 않으면 인덱스를 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;WHERE 조건절과 달리 GROUP BY는 인덱스가 하나라도 명시되어있지 않으면 전혀 인덱스를 사용하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ORDER BY 절 인덱스 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY 절의 조건과 일치하고 하나더 조건이 있는데 정렬되는 각 컬럼의 오름차순 및 내림차순 옵션이 인덱스와 같거나 정반대인 경우에만 사용할 수 있다. 예를 들면, 인덱스의 모든 컬럼이 오름차순으로 정렬되어있다면 order by절은 모든 컬럼이 오름차순이거나 내림차순일 때만 사용할 수 있다. 그리고 모든 인덱스가 order by에 명시되어야 하는 것은 아니지만 순서는 일치해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WHERE + ORDER BY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where절과 order by 절이 같이 사용된 쿼리는 다음 3가지 중 하나로 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절과 ORDER BY 절이 동시에 같은 인덱스를 이용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where 절의 비교 조건에서 사용하는 컬럼과 order by 절의 정렬 대상 컬럼이 모두 하나의 인덱스를 연속해서 포함돼 있을 때 이 방식으로 인덱스를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;WHERE 절만 인덱스를 이용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;order by 절은 인덱스를 이용한 정렬이 불가능하며, 인덱스를 통해 검색된 결과 레코드를 별도의 정렬 처리 과정(file sort)을 거쳐 정렬을 수행한다. 주로 WHERE 조건절에 일치하는 레코드 건수가 많지 않을 때 효율적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ORDER BY 절만 인덱스를 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY 절의 순서대로 인덱스를 읽으면서 레코드 한 건씩 where 조건에 일치하는지 비교하고 일치하지 않을 때는 버리는 형태로 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 where 절에서 조건으로 사용된 컬럼과 order by 절에 명시된 컬럼 순서가 인덱스 컬럼의 왼쪽부터 일치해야 한다. 중간에 빠지는 부분이 있으면 모두 인덱스를 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUP BY&amp;nbsp;+ ORDER BY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 절 모두 하나의 인덱스를 사용해서 처리되려면 둘다 명시된 컬럼의 순서와 내용이 모두 같아야 한다. 둘중 하나라도 인덱스를 이용할 수 없을때는 둘다 인덱스를 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; WHERE + ORDER BY + GROUP BY&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;where 절이 인덱스를 탈 수 있는가?&lt;/li&gt;
&lt;li&gt;group by 절이 인덱스를 사용할 수 있는가?&lt;/li&gt;
&lt;li&gt;group by 절과 order by 절이 동시에 인덱스를 사용할 수 있는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 세가지 조건을 만족하면 인덱스를 태울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;limit&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select * from .. limit 0 10;&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;limit이 없다면 풀 테이블 스캔이 실행된다. 위는 limit 조건이 있으므로 풀 테이블 스캔을 실행하면서 엔진은 10개의 레코드를 읽어들이는 순간 작업을 멈춘다. 이렇게 정렬, 그루핑, distinct가 없으면 쿼리가 상당히 빨리 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;selecgt * from .. group by .. limit 0 10;&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;group by가 있으므로 group by가 종료되야 limit을 처리할 수 있다. 따라서 limit이 있더라도 작업을 크게 줄여주진 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select distinct .. from .. limit 0 10;&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;엔진은 풀 테이블 스캔을 하면서 레코드를 읽음과 동시에 distinct 중복 제거 작업(임시 테이블 사용)을 진행한다. 이 작업을 처리하다가 유니크한 레코드가 limit 건수만큼 채워지면 쿼리를 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select * from .. where .. order by .. limit 0 10;&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;where 조건에 맞는 레코드를 읽고 정렬을 수행한다. 정렬을 수행하면서 10건이 완성되면 쿼리를 종료한다. 레코드를 전부 읽고 정렬을 해야하므로 작업량이 크게 줄지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;count&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드 건수를 반환하는 것으로 count(*) 를 사용할 수 있다. 여기서 *는 레코드 자체를 의미하므로 프라이머리 컬럼을 넣거나 1을 넣는 것이랑 똑같이 동작한다. 보통 쿼리 작성할 때 기존 쿼리에서 프로젝션 부분만 count로 바꿔서 사용하는 경우가 있는데 이러면 불필요한 order by 같은 것이 들어가는 경우가 발생할 수 있다. 그래서 8.0부터는 count 쿼리에서 order by는 무시하도록 옵티마이저가 처리한다. 하지만 이를 알고 있더라도 굳이 쿼리의 복잡도가 높아보이도록 작성할 필요는 없기에 count 쿼리에 불필요한 절을 제거할 수 있도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;join&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;join의 순서와 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인에서 드라이빙 테이블을 읽을 때는 인덱스 탐색 작업을 단 한번만 수행하고 이후부터는 스캔만 하면 된다. 하지만 드리븐 테이블에서는 인덱스 탐색 작업과 스캔 작업을 드라이빙 테이블을 읽은 레코드 건수만큼 반복한다. 그래서 옵티마이저는 드라이빙 테이블보다 드리븐 테이블을 최적으로 읽을 수 있게 실행계획을 수립한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;outer join 성능과 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아우터 조인시 실수하는 상황은 아우터 조인되는 테이블에 대한 조건을 where 조건 절에 사용하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select * 
from employee e
    left join dept_manager mgr on mgr.emp_no = e.emp_no
    where mgr.dept_no='001';&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;on 절에 조인 조건을 명시하고 where 조건절에 outer 조인 되는 드리븐 테이블의 컬럼을 조건으로 사용했다. 하지만 &lt;b&gt;옵티아미저는 아우터 조인으로 사용된 드리븐 테이블의 컬럼을 where 조건으로 사용한 것을 보고 해당 쿼리를 inner join으로 바꿔버린다. 따라서 정상적으로 아우터 조인이 되게 만들려면 다음과 같이 where절의 조건을 on절로 옮겨야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select * 
from employee e
    left join dept_manager mgr on mgr.emp_no = e.emp_no AND mgr.dept_no='001'; &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;안티 조인 효과&lt;/b&gt;를 기대하는 단 한가지의 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;select * 
from employee e
    left join dept_manager mgr on mgr.emp_no = e.emp_no
    where mgr.dept_no IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 NULL인 레코들만 조회한다. 이것이 아우터 드리븐 테이블이 조인에서 where 조건절로 사용할 수 있는 유일한 경우다. 그 외의 경우에는&lt;b&gt; 아우터 드리븐 테이블이 where 조건절로 오면 MySQL 서버는 LEFT JOIN을 inner join으로 변경해버린다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잠금을 사용하는 SELECT&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드를 select할때는 lock 없이 진행되는데 select가 실행된 후 애플리케이션에서 가공해서 업데이트 할때는 다른 트랜잭션이 그 컬럼 값을 변경하지 못하게 해야 한다. 이럴 때는 읽으면서 레코드에 잠금을 걸어 둘 필요가 있다. 이때 사용하는 옵션이 for share와 for update 절이다. for share는 읽기 잠금이고 for update는 select 쿼리가 읽은 레코드에 대해 쓰기 잠금을 건다. 해당 옵션이 사용되었더라고 일반적인 select 구문은 그대로 대기 없이 데이터를 읽어갈 수 있다. 즉, 해당 옵션이 사용된 쿼리끼리만 잠금이 걸리는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잠금 테이블 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8.0 이전에서는 조인한 테이블 전체에 모두 잠금이 걸렸다. 하지만 조인된 테이블은 참고용이라면 8.0에서는 쿼리에 사용된 테이블 중 특정 테이블만 잠금을 획득하는 옵션으로 &lt;code&gt;for share(for update) of 테이블 별칭&lt;/code&gt; 이 추가되었다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>Database</category>
      <category>index</category>
      <category>mysql</category>
      <category>MySql8.0</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/60</guid>
      <comments>https://backtony.tistory.com/60#entry60comment</comments>
      <pubDate>Mon, 1 Jan 2024 23:13:39 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 실행계획 보는 방법</title>
      <link>https://backtony.tistory.com/59</link>
      <description>&lt;h1&gt;쿼리튜닝을 위한 인덱스 특징(innoDB, B-Tree)&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스는 정렬되어 있다.&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1208&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bP9Xev/btsCU0HSbNe/xew6Frb4FnBmJxmAdhye1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bP9Xev/btsCU0HSbNe/xew6Frb4FnBmJxmAdhye1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bP9Xev/btsCU0HSbNe/xew6Frb4FnBmJxmAdhye1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbP9Xev%2FbtsCU0HSbNe%2Fxew6Frb4FnBmJxmAdhye1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1208&quot; height=&quot;496&quot; data-origin-width=&quot;1208&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왼쪽 컬럼인 column_1 순으로 먼저 정렬되고, column_1이 같다면 column_2 순으로 정렬된다.&lt;/li&gt;
&lt;li&gt;따라서 인덱스를 기준으로 조회한 데이터는 인덱스 컬럼 순으로 정렬되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스는 스캔하는 방식으로 처리된다.&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cawte2/btsCN1BMa0k/ICEh5VJoOOnWKW1lxmkdg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cawte2/btsCN1BMa0k/ICEh5VJoOOnWKW1lxmkdg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cawte2/btsCN1BMa0k/ICEh5VJoOOnWKW1lxmkdg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcawte2%2FbtsCN1BMa0k%2FICEh5VJoOOnWKW1lxmkdg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1630&quot; height=&quot;506&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간중간 데이터를 읽는 것이 아니라, 시작지점을 잡고 종료지점까지 쭉 스캔한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스는 메모리 위에 존재한다.&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5yfCF/btsCMzk4Kko/11WHwk0kQMZ4KBbmRIrjTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5yfCF/btsCMzk4Kko/11WHwk0kQMZ4KBbmRIrjTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5yfCF/btsCMzk4Kko/11WHwk0kQMZ4KBbmRIrjTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5yfCF%2FbtsCMzk4Kko%2F11WHwk0kQMZ4KBbmRIrjTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1808&quot; height=&quot;910&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;column_1 = 'A'인 레코드 중에 column_3 이 필요하다면?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 column_1 = 'A'인 레코드를 모두 가져온 다음 물리 주소를 파악해서 물리 주소를 기반으로 디스크에서 값을 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 컬럼 값을 인덱스에 올려놓을 수는 없기 때문에 성능적으로 타협하면서 세팅해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 좋은 Where 절의 조건 = 인덱스를 잘 활용하는 조건&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스는 정렬되어 있으므로, 정렬된 데이터를 최대한도로 활용한다.(추가 정렬 작업을 수행하지 않도록)&lt;/li&gt;
&lt;li&gt;인덱스는 스캔하는 방식으로 처리되므로, 적절한 범위를 스캔할 수 있도록 구성한다.&lt;/li&gt;
&lt;li&gt;인덱스는 메모리 위에 존재하므로, 가능한 인덱스 내의 데이터를 조회한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2310&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dikpdC/btsCOFSKcXP/4JXh7vGtHHL8mlOcDkkLd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dikpdC/btsCOFSKcXP/4JXh7vGtHHL8mlOcDkkLd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dikpdC/btsCOFSKcXP/4JXh7vGtHHL8mlOcDkkLd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdikpdC%2FbtsCOFSKcXP%2F4JXh7vGtHHL8mlOcDkkLd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2310&quot; height=&quot;650&quot; data-origin-width=&quot;2310&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A로 끝나는 문자열에 대한 시작지점, 끝지점을 적당히 설정할 수 없기 때문에 모든 데이터를 읽어 들이게 된다.&lt;/li&gt;
&lt;li&gt;B로 시작하는 문자열 조회는, B로 시작하는 문자열의 시작지점과 끝지점을 알 수 있기 때문에 불필요한 데이터를 읽지 않을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;실행계획&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿼리튜닝을 위한 실행계획 보는 법&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmFTcu/btsCUXYF89e/bzOhvEuCHPpF90zk8sQfzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmFTcu/btsCUXYF89e/bzOhvEuCHPpF90zk8sQfzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmFTcu/btsCUXYF89e/bzOhvEuCHPpF90zk8sQfzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmFTcu%2FbtsCUXYF89e%2FbzOhvEuCHPpF90zk8sQfzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;600&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;select 쿼리 앞에 &lt;code&gt;explain&lt;/code&gt;을 붙여서 실행한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 상세한 결과를 원한다면 explain extended, explain partitions 가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;update / delete/ insert 구문은 실행계획을 확인할 방법이 따로 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 where 절을 가지는 select 구문의 실행계획을 확인하여 대략적으로 파악할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행계획 표기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;id&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2444&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x3BG8/btsCN3zyziG/vcuuK9qoq7GzJq4QJAElgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x3BG8/btsCN3zyziG/vcuuK9qoq7GzJq4QJAElgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3BG8/btsCN3zyziG/vcuuK9qoq7GzJq4QJAElgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx3BG8%2FbtsCN3zyziG%2FvcuuK9qoq7GzJq4QJAElgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2444&quot; height=&quot;604&quot; data-origin-width=&quot;2444&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;select 쿼리 별로 부여되는 식별자 값&lt;/li&gt;
&lt;li&gt;서브쿼리를 사용하는 경우에는 id가 2 이상으로 표기될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;select_type&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2350&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v5ha3/btsCQ5KaRIT/JMdfziwUv5giqWdgIJ6eP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v5ha3/btsCQ5KaRIT/JMdfziwUv5giqWdgIJ6eP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v5ha3/btsCQ5KaRIT/JMdfziwUv5giqWdgIJ6eP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv5ha3%2FbtsCQ5KaRIT%2FJMdfziwUv5giqWdgIJ6eP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2350&quot; height=&quot;816&quot; data-origin-width=&quot;2350&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 단위 select 쿼리(id별로 구별되는 쿼리)가 어떤 타입의 쿼리인지 표기한다.&lt;/li&gt;
&lt;li&gt;색칠된 코멘트는 개선이 필요할 수 있는 코멘트에 해당한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;UNCACACHEABLE&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/86Zv5/btsCQrGApkD/0zPJKq59utnDGF8w3NseW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/86Zv5/btsCQrGApkD/0zPJKq59utnDGF8w3NseW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/86Zv5/btsCQrGApkD/0zPJKq59utnDGF8w3NseW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F86Zv5%2FbtsCQrGApkD%2F0zPJKq59utnDGF8w3NseW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1802&quot; height=&quot;944&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사원번호를 random으로 추출한다음 union All하고 있는데 rand()는 항상 값이 달라지므로 캐시 할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btVqhg/btsCN5RyGzQ/k15hJwNpYy3e38VohiPzfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btVqhg/btsCN5RyGzQ/k15hJwNpYy3e38VohiPzfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btVqhg/btsCN5RyGzQ/k15hJwNpYy3e38VohiPzfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtVqhg%2FbtsCN5RyGzQ%2Fk15hJwNpYy3e38VohiPzfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1808&quot; height=&quot;862&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무작위 정렬이 필수적인 테이블이라면 임의의 값을 저장하는 컬럼을 만들고 데이터를 생성할 때마다 무작위 값을 넣어준다.&lt;/li&gt;
&lt;li&gt;무작위 컬럼에 인덱스를 생성하여 성능을 올린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;dependent&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxEFzx/btsCMzL7cq9/o5y0M8Y6FEkgeu9nEx5iGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxEFzx/btsCMzL7cq9/o5y0M8Y6FEkgeu9nEx5iGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxEFzx/btsCMzL7cq9/o5y0M8Y6FEkgeu9nEx5iGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxEFzx%2FbtsCMzL7cq9%2Fo5y0M8Y6FEkgeu9nEx5iGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1808&quot; height=&quot;862&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dependent는 독립적으로 수행할 수 없어, 외부 쿼리 결과에 의존하는 쿼리이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1832&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tgfSK/btsCRgY4Aql/bOZKnnMIK415mcBPBTQil0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tgfSK/btsCRgY4Aql/bOZKnnMIK415mcBPBTQil0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tgfSK/btsCRgY4Aql/bOZKnnMIK415mcBPBTQil0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtgfSK%2FbtsCRgY4Aql%2FbOZKnnMIK415mcBPBTQil0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1832&quot; height=&quot;938&quot; data-origin-width=&quot;1832&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dependent는 join으로 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;derived&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1872&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4skBP/btsCUZCa2bb/KLbxySBKSPkgoRZwgzX6kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4skBP/btsCUZCa2bb/KLbxySBKSPkgoRZwgzX6kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4skBP/btsCUZCa2bb/KLbxySBKSPkgoRZwgzX6kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4skBP%2FbtsCUZCa2bb%2FKLbxySBKSPkgoRZwgzX6kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1872&quot; height=&quot;916&quot; data-origin-width=&quot;1872&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;from 절에서 서브쿼리가 사용되는 경우에 해당한다.&lt;/li&gt;
&lt;li&gt;데이터가 적은 경우에는 문제가 없으나 데이터가 커지면 문제가 생긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cf) 임시 테이블&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회한 레코드를 추가적으로 정렬하거나 그룹핑해야 할 때, 사용하는 일시적인 테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 잘 사용했다면, 추가적인 정렬이 필요 없으므로 임시테이블을 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기본적으로는 메모리 위에서 동작하지만, 지정된 크기를 넘어서는 데이터가 조회되는 경우 디스크를 사용하게 됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스크를 사용하는 임시테이블은 큰 속도저하가 있으므로 사용하지 않도록 튜닝이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시테이블을 사용하는 조건들이 꽤나 복잡해서 임시테이블을 사용하지 않는 조건을 먼저 알아보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY와 GROUP BY는 가능한 같은 인덱스에 존재하는 컬럼들로 처리한다.&lt;/li&gt;
&lt;li&gt;ORDER BY와 GROUP BY는 가능한 인덱스 컬럼의 순서대로 정렬/그룹핑을 진행한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가적인 정렬/그룹핑을 최소화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;필요한 값만 조회한다. (SELECT * FROM 금지)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 데이터를 조회해서 임시테이블이 디스크에서 사용되는 상황을 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정렬이 필요한 경우라면 DISTINCT 보다는 GROUP BY를 사용한다.&lt;/li&gt;
&lt;li&gt;불필요한 서브쿼리는 가능한 조인으로 변경한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 경우 임시테이블을 사용하게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가적인 정렬/그룹핑이 필요한 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY와 GROUP BY에 명시된 컬럼이 다른 경우&lt;/li&gt;
&lt;li&gt;ORDER BY나 GROUP BY에 명시된 컬럼이 조인의 순서상 첫 번째 테이블이 아닌 경우&lt;/li&gt;
&lt;li&gt;DISTINCT와 ORDER BY가 동시에 존재하는 경우&lt;/li&gt;
&lt;li&gt;DISTINCT가 인덱스로 처리되지 못 한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UNION ALL이 사용된 경우&lt;/li&gt;
&lt;li&gt;쿼리의 실행 계획에서 select_type이 &amp;ldquo;DERIVED&amp;rdquo; 인 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;type&lt;/h3&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;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEP8de/btsCQ5clZOU/lvFAuY1LggkYfuPWITiZC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEP8de/btsCQ5clZOU/lvFAuY1LggkYfuPWITiZC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEP8de/btsCQ5clZOU/lvFAuY1LggkYfuPWITiZC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEP8de%2FbtsCQ5clZOU%2FlvFAuY1LggkYfuPWITiZC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1718&quot; height=&quot;570&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 서버가 어떤 방식으로 레코드를 읽었는지를 표시해주는 컬럼으로 튜닝 시에 중요하게 확인해 봐야 하는 컬럼&lt;/li&gt;
&lt;li&gt;ALL 을 제외하고는 모두 인덱스를 사용하지만, 그렇다고 모두 효율적인 것은 아니다.&lt;/li&gt;
&lt;li&gt;위 표는 효율적인 순서대로 작성되었다.(빨간색은 매우 나쁨)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index는 인덱스를 사용했다는 게 아니라 인덱스 풀스캔했다는 뜻으로 문제가 있다는 의미다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 레인지 스캔&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zB5Pt/btsCRhReoQz/ZcmiiIR7CA2rsWiupl9Kx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zB5Pt/btsCRhReoQz/ZcmiiIR7CA2rsWiupl9Kx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zB5Pt/btsCRhReoQz/ZcmiiIR7CA2rsWiupl9Kx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzB5Pt%2FbtsCRhReoQz%2FZcmiiIR7CA2rsWiupl9Kx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;868&quot; height=&quot;562&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 범위의 데이터만 조회하는 방식&lt;/li&gt;
&lt;li&gt;리프노드의 시작 위치를 찾고 해당 지점부터 마지막 범위에 해당하는 위치까지 순차적으로 조회&lt;/li&gt;
&lt;li&gt;조회해야 하는 데이터가 너무 많아지는 경우, 풀 테이블 스캔이 더 효율적일 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 너무 많으면 사실상 인덱스를 다 스캔하는 것과 별 차이가 없기 때문이다.&lt;/li&gt;
&lt;li&gt;이런 경우를 막기 위해 where절을 잘 설정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 풀 스캔&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qx4V3/btsCRf0dBez/hPAGvq1R19SWszUKI6eRb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qx4V3/btsCRf0dBez/hPAGvq1R19SWszUKI6eRb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qx4V3/btsCRf0dBez/hPAGvq1R19SWszUKI6eRb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqx4V3%2FbtsCRf0dBez%2FhPAGvq1R19SWszUKI6eRb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;624&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 효율적으로 활용하지 못한 스캔 방식&lt;/li&gt;
&lt;li&gt;인덱스의 데이터를 전부 조회한다.&lt;/li&gt;
&lt;li&gt;인덱스 풀 스캔으로 처리되지 않기 위해서는 인덱스 컬럼의 왼쪽 컬럼부터 조건으로 사용해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where 절 내에서의 순서는 상관없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파일 I/O가 필요한 경우라면, 매우 비효율적으로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;루스(Loose) 인덱스 스캔&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7czgd/btsCRg5Twxl/ymkM3KOZ8N3XOcbhSHu89K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7czgd/btsCRg5Twxl/ymkM3KOZ8N3XOcbhSHu89K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7czgd/btsCRg5Twxl/ymkM3KOZ8N3XOcbhSHu89K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7czgd%2FbtsCRg5Twxl%2FymkM3KOZ8N3XOcbhSHu89K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;870&quot; height=&quot;598&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스는 기본적으로 정렬되어 있기 때문에 집합함수 중 정렬을 활용할 수 있는 함수들(MAX, MIN)은 인덱스를 모두 훑지 않고 건너뛰면서 스캔한다.&lt;/li&gt;
&lt;li&gt;반대로 AVG, COUNT 집합 함수들은 인덱스 전체를 스캔해야 하는데 이런 경우를 Tight 인덱스 스캔이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;풀 테이블 스캔&lt;/h4&gt;
&lt;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;where 절이나 on 절에 인덱스를 이용할 수 있는 적절한 조건이 없는 경우(인덱스를 사용할 수 없는 경우)&lt;/li&gt;
&lt;li&gt;인덱스 레인지 스캔을 할 수 있더라도, 옵티마이저가 풀 테이블 스캔이 더 효율적이라고 판단하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;B-Tree를 샘플링해서 저장해 둔 통계정보를 기준으로 판단&lt;/li&gt;
&lt;li&gt;인덱스 레인지 스캔으로 조회해야 하는 레코드 수가 전체 레코드 수의 20~25%일 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;possible_keys, key&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGPGfq/btsCUAP47JS/DAngzlpvZVwUHvm6V4WO81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGPGfq/btsCUAP47JS/DAngzlpvZVwUHvm6V4WO81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGPGfq/btsCUAP47JS/DAngzlpvZVwUHvm6V4WO81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGPGfq%2FbtsCUAP47JS%2FDAngzlpvZVwUHvm6V4WO81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1758&quot; height=&quot;394&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;possible_keys : 쿼리를 처리할 때 사용될 법했던 인덱스의 목록&lt;/li&gt;
&lt;li&gt;key : 실제로 선택된 인덱스&lt;/li&gt;
&lt;li&gt;인덱스는 옵티마이저가 자체적으로 판단하여 선택하지만 필요한 경우 force index 등의 예약어를 통해 강제할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;extra&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;튜닝 시에 중요하게 확인해야 하는 컬럼&lt;code&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요구사항 확인이 필요한 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b24s3j/btsCQ27Jo11/AKOZSEjk3E5kdoVzqpp8Jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b24s3j/btsCQ27Jo11/AKOZSEjk3E5kdoVzqpp8Jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b24s3j/btsCQ27Jo11/AKOZSEjk3E5kdoVzqpp8Jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb24s3j%2FbtsCQ27Jo11%2FAKOZSEjk3E5kdoVzqpp8Jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1700&quot; height=&quot;514&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리가 요구사항을 제대로 반영하고 있는지 확인해야 하는 코멘트&lt;/li&gt;
&lt;li&gt;성능적으로는 문제가 없지만 DB 내에 필요한 값이 존재하지 않는 경우를 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 계획이 좋지 못한 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHLcGo/btsCUY4nAB4/K21PDihwFP4DZixGvHwqC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHLcGo/btsCUY4nAB4/K21PDihwFP4DZixGvHwqC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHLcGo/btsCUY4nAB4/K21PDihwFP4DZixGvHwqC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHLcGo%2FbtsCUY4nAB4%2FK21PDihwFP4DZixGvHwqC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1734&quot; height=&quot;502&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 코멘트가 조회되면 더 최적화할 수 있는지 검토가 필요하다.&lt;/li&gt;
&lt;li&gt;불필요한 작업들을 추가로 하는 경우가 있을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 계획이 좋은 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mCZ9f/btsCQq8LUip/BPWp7sHYgb3xiDdZRmj1Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mCZ9f/btsCQq8LUip/BPWp7sHYgb3xiDdZRmj1Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mCZ9f/btsCQq8LUip/BPWp7sHYgb3xiDdZRmj1Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmCZ9f%2FbtsCQq8LUip%2FBPWp7sHYgb3xiDdZRmj1Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1744&quot; height=&quot;340&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;340&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;h1&gt;해결 방식 예시&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNlnCj/btsCOGc3mFM/iCnGEJR31OKYOkz5EAsl0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNlnCj/btsCOGc3mFM/iCnGEJR31OKYOkz5EAsl0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNlnCj/btsCOGc3mFM/iCnGEJR31OKYOkz5EAsl0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNlnCj%2FbtsCOGc3mFM%2FiCnGEJR31OKYOkz5EAsl0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1660&quot; height=&quot;956&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;956&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;1708&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chhfK9/btsCTV7TyaD/3t6KkiryCyrtHLNiwMiVh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chhfK9/btsCTV7TyaD/3t6KkiryCyrtHLNiwMiVh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chhfK9/btsCTV7TyaD/3t6KkiryCyrtHLNiwMiVh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchhfK9%2FbtsCTV7TyaD%2F3t6KkiryCyrtHLNiwMiVh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1708&quot; height=&quot;850&quot; data-origin-width=&quot;1708&quot; data-origin-height=&quot;850&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;1784&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vdTxL/btsCUZhSIVX/A6fyvC42RkgZiz7iFjjlh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vdTxL/btsCUZhSIVX/A6fyvC42RkgZiz7iFjjlh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vdTxL/btsCUZhSIVX/A6fyvC42RkgZiz7iFjjlh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvdTxL%2FbtsCUZhSIVX%2FA6fyvC42RkgZiz7iFjjlh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1784&quot; height=&quot;812&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Database</category>
      <category>mysql</category>
      <category>실행계획</category>
      <category>튜닝</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/59</guid>
      <comments>https://backtony.tistory.com/59#entry59comment</comments>
      <pubDate>Sun, 31 Dec 2023 14:31:54 +0900</pubDate>
    </item>
    <item>
      <title>Spring - hikariCP 옵션 정리 및 권장 설정</title>
      <link>https://backtony.tistory.com/58</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;datasource&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot 2.x.x 기준에서 사용 가능한 connection pool은 3가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hikariCP&lt;/li&gt;
&lt;li&gt;tomcat pooling datasource&lt;/li&gt;
&lt;li&gt;common dbcp2&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot 1.x.x에서는 tomcat이 default였으나 2.0.0부터 hikaricp가 default가 되었습니다. 그래서 spring-boot-starter-jdbc, spring-boot-starter-data-jpa를 사용하면, HikariCP(가장 높음) -&amp;gt; Tomcat pooling -&amp;gt; Commons DBCP2의 순위로 지정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 우선순위를 무시하고 spring.datasource.type에 값을 통해 설정할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;hikaricp의 옵션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;auto-commit (default : true)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection이 종료되거나 pool에 반환될 때, connection에 속해있는 transaction을 commit 할지를 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;connection-timeout(default: 30000 - 30 seconds)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pool에서 커넥션을 얻어오기전까지 기다리는 최대 시간, 허용가능한 wait time을 초과하면 SQLException을 던짐&lt;/li&gt;
&lt;li&gt;설정가능한 가장 작은 시간은 250ms (default: 30000 (30s))&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;idle-timeout(default : 600000 - 10 minutes)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pool에 일을 안하는 커넥션을 유지하는 시간&lt;/li&gt;
&lt;li&gt;이 옵션은 minimum-idle이 maximum-pool-size보다 작게 설정되어 있을 때만 설정&lt;/li&gt;
&lt;li&gt;pool에서 유지하는 최소 커넥션 수는 minimum-idle(A connection will never be retired as idle before this timeout)&lt;/li&gt;
&lt;li&gt;최솟값은 10000ms (default: 600000 (10minutes))&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;max-lifetime(default : 1800000 - 30 minutes)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간&lt;/li&gt;
&lt;li&gt;사용 중인 커넥션은 max-lifetime에 상관없이 제거되지 않음 사용 중이지 않을 때만 제거됨&lt;/li&gt;
&lt;li&gt;pool 전체가 아닌 커넥션 별로 적용이 되는데 그 이유는 풀에서 대량으로 커넥션들이 제거되는 것을 방지하기 위함임&lt;/li&gt;
&lt;li&gt;강력하게 설정해야 하는 설정 값으로 데이터베이스나 인프라의 적용된 connection time limit보다 작아야 함&lt;/li&gt;
&lt;li&gt;0으로 설정하면 infinite lifetime이 적용됨&lt;/li&gt;
&lt;li&gt;(idle-timeout설정 값에 따라 적용 idle-timeout값이 설정되어 있을 경우 0으로 설정해도 무한 lifetime 적용 안됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;minimum-idle (default : maximum-pool-size)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아무런 일을 하지 않아도 적어도 이 옵션에 설정 값 size로 커넥션들을 유지해 주는 설정&lt;/li&gt;
&lt;li&gt;최적의 성능과 응답성을 요구한다면 이 값은 설정하지 않는 게 좋음&lt;/li&gt;
&lt;li&gt;default값을 보면 이해할 수 있음&lt;/li&gt;
&lt;li&gt;HikariCP에서는 최고의 performance를 위해 maximum-pool-size와 minimum-idle값을 같은 값으로 지정해서 connection Pool의 크기를 fix 하는 것을 강력하게 권장한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;maximum-pool-size (default: 10)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pool에 유지시킬 수 있는 최대 커넥션 수&lt;/li&gt;
&lt;li&gt;pool의 커넥션 수가 옵션 값에 도달하게 되면 idle인 상태는 존재하지 않음.&lt;/li&gt;
&lt;li&gt;성능 테스트 시 DB 부하에 따라 결정합니다. (DB CPU 부하가 높다면 풀 개수를 줄이고, 부하가 낮다면 풀 개수를 증가시킵니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pool-name (default : auto-generated)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 옵션은 사용자가 pool의 이름을 지정함&lt;/li&gt;
&lt;li&gt;logging이나 JMX management console에 표시되는 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;initialization-fail-timeout&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pool에서 커넥션을 초기화할 때 성공적으로 수행할 수 없을 경우 빠르게 실패하도록 해준다&lt;/li&gt;
&lt;li&gt;상세 내용은 한국말보다 원문이 더 직관적이라 생각되어 다음 글을 인용함&lt;/li&gt;
&lt;li&gt;This property controls whether the pool will &amp;ldquo;fail fast&amp;rdquo; if the pool cannot be seeded with an initial connection successfully. Any positive number is taken to be the number of milliseconds to attempt to acquire an initial connection; the application thread will be blocked during this period. If a connection cannot be acquired before this timeout occurs, an exception will be thrown. This timeout is applied after the connectionTimeout period. If the value is zero (0), HikariCP will attempt to obtain and validate a connection. If a connection is obtained, but fails validation, an exception will be thrown and the pool not started. However, if a connection cannot be obtained, the pool will start, but later efforts to obtain a connection may fail. A value less than zero will bypass any initial connection attempt, and the pool will start immediately while trying to obtain connections in the background. Consequently, later efforts to obtain a connection may fail. Default: 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;validation-timeout&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;valid 쿼리를 통해 커넥션이 유효한지 검사할 때 사용되는 timeout&lt;/li&gt;
&lt;li&gt;최소 250ms 이상부터 설정가능(default: 5000ms)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;connection-test-query (default : none)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC4 드라이버를 지원한다면 이 옵션은 설정하지 않는 것을 추천&lt;/li&gt;
&lt;li&gt;이 옵션은 JDBC4를 지원 안 하는 드라이버를 위한 옵션임(Connection.isValid() API)&lt;/li&gt;
&lt;li&gt;커넥션 pool에서 커넥션을 획득하기 전에 살아있는 커넥션인지 확인하기 위해 valid 쿼리를 던지는 데 사용되는 쿼리&lt;/li&gt;
&lt;li&gt;(보통 SELECT 1로 설정)&lt;/li&gt;
&lt;li&gt;JDBC4 드라이버를 지원하지 않는 환경에서 이 값을 설정하지 않는다면 error레벨 로그를 뱉어냄&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;read-only (default : false)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pool에서 커넥션을 획득할 때 read-only 모드로 가져옴&lt;/li&gt;
&lt;li&gt;몇몇의 database는 read-only모드를 지원하지 않음&lt;/li&gt;
&lt;li&gt;커넥션이 read-only로 설정되어 있으면 몇몇의 쿼리들이 최적화됨&lt;/li&gt;
&lt;li&gt;이 설정은 database에서 지원하지 않는다면 readOnly가 아닌 상태로 open 되기 때문에, 지원되는 database 목록을 확인해 보고 사용해야 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;transaction-isolation (default : none)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;java.sql.Connection 에 지정된 Transaction Isolation을 지정한다&lt;/li&gt;
&lt;li&gt;지정된 Transaction Isoluation은 다음과 같다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection.TRANSACTION_NONE : transaction을 지원하지 않는다&lt;/li&gt;
&lt;li&gt;Connection.TRANSACTION_READ_UNCOMMITTED : transaction이 끝나지 않았을 때, 다른 transaction에서 값을 읽는 경우 commit 되지 않은 값(dirty value)을 읽는다&lt;/li&gt;
&lt;li&gt;Connection.TRANSACTION_READ_COMMITTED : transaction이 끝나지 않았을 때, 다른 transaction에서 값을 읽는 경우 변경되지 않은 값을 읽는다&lt;/li&gt;
&lt;li&gt;Connection.TRANSACTION_REPEATABLE_READ : 같은 transaction 내에서 값을 또다시 읽을 때, 변경되기 전의 값을 읽는다 TRANSACTION_READ_UNCOMMITTED 와 같이 사용될 수 없다&lt;/li&gt;
&lt;li&gt;Connection.TRANSACTION_SERIALIZABLE : dirty read를 지원하고, non-repeatable read를 지원한다&lt;/li&gt;
&lt;li&gt;기본값을 각 Driver vendor의 JDBCDriver에서 지원하는 Transaction Isoluation을 따라간다. (none으로 설정 시)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;category (default : none)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;connection에서 연결할 category를 결정한다&lt;/li&gt;
&lt;li&gt;값이 설정되지 않는 경우, JDBC Driver에서 설정된 기본 category를 지정하게 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;leak-detection-threshold (default : 0)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션이 누수 로그메시지가 나오기 전에 커넥션을 검사하여 pool에서 커넥션을 내보낼 수 있는 시간&lt;/li&gt;
&lt;li&gt;0으로 설정하면 leak detection을 이용하지 않음&lt;/li&gt;
&lt;li&gt;최솟값 2000ms (default: 0)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Statement Cache&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 connection pool(including dbcp, vibur, c3p0)라이브러리들은 PreparedStatement caching을 지원하지만 HikariCP는 지원하지 않는다&lt;/li&gt;
&lt;li&gt;connection pool layer에서 PreparedStatements는 각 커넥션 마다 캐싱된다&lt;/li&gt;
&lt;li&gt;애플리케이션에서 250개의 공통적인 쿼리를 캐싱하고 있고 커넥션 pool size가 20이라면 database에 5000 쿼리 실행계획을 hold 하고 있게 된다&lt;/li&gt;
&lt;li&gt;대부분의 major database의 jdbc driver들은 이미 설정을 통해 Statement를 캐싱할 수 있다&lt;/li&gt;
&lt;li&gt;(PostgreSQL, Oracle, Derby, MySQL, DB2 등등) 즉 우리가 원하는 대로 250개의 쿼리 실행계획만 데이터베이스에 캐싱할 수 있음을 의미한다&lt;/li&gt;
&lt;li&gt;(connection pool에 설정하는 것이 아닌 driver에 설정함으로써)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;driver-class-name (default : none)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HikariCP는 jdbcUrl을 참조하여 자동으로 driver를 설정하려고 시도함&lt;/li&gt;
&lt;li&gt;하지만 몇몇의 오래된 driver들은 driver-class-name을 명시화 해야 함&lt;/li&gt;
&lt;li&gt;어떤 에러 메시지가 명백하게 표시되지 않는다면 생략해도 됨&lt;/li&gt;
&lt;li&gt;이 값이 지정되는 경우, jdbcUrl이 반드시 설정되어야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;jdbc-url&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jdbcUrl을 지정한다&lt;/li&gt;
&lt;li&gt;driverClassName이 지정된 경우, jdbcUrl을 반드시 지정해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;registerMbeans (default : false)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JMX management Beans에 등록되는 될지 여부를 지정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;username&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection을 얻어내기 위해서 사용되는 인증 이름을 넣는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;password&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;username과 쌍이 되는 비밀번호를 지정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;게임 서버 시스템을 위한 HikariCP 옵션 권장 설정&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;2076&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsv6VQ/btsCUZ3hRPM/OdLqEeb2DKoNWFVLNKBjxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsv6VQ/btsCUZ3hRPM/OdLqEeb2DKoNWFVLNKBjxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsv6VQ/btsCUZ3hRPM/OdLqEeb2DKoNWFVLNKBjxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsv6VQ%2FbtsCUZ3hRPM%2FOdLqEeb2DKoNWFVLNKBjxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1764&quot; height=&quot;2076&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;2076&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 설정은 넷마블 기술 블로그에 있는 권장 설정입니다. 자세한 내용은 &lt;a href=&quot;https://netmarble.engineering/hikaricp-options-optimization-for-game-server/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;spring boot 적용법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hikaricp의 옵션은 application.yml에 명시하면 됩니다. 관련 설정이 자동완성이 안되는데 &lt;b&gt;com.zaxxer.hikari.HikariConfig&lt;/b&gt; 파일에서 매핑되는 필드를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
    datasource:
        hikari:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: url
            username: username
            password: password
            maximum-pool-size: 5
            minimum-idle: 5
            connection-timeout: 5000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration&quot;&gt;hikaricp mysql config&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP&quot;&gt;hikaricp github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/5102792&quot;&gt;Commons DBCP 이해하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://netmarble.engineering/hikaricp-options-optimization-for-game-server/&quot;&gt;게임 서버 시스템을 위한 hikaricp 권장 옵션&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/296&quot;&gt;Spring Boot &amp;amp; HikariCP Datasource 연동하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>dataSource</category>
      <category>hikaricp</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/58</guid>
      <comments>https://backtony.tistory.com/58#entry58comment</comments>
      <pubDate>Sun, 31 Dec 2023 14:09:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring - restTemplate 대용량 파일 업로드, 다운로드 설정</title>
      <link>https://backtony.tistory.com/57</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 포스팅은 kotlin과 java 코드가 혼재되어 있으며 spring boot 3.x 버전에서 사용된 코드입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RestTemplate이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 외부 요청을 위해 RestTemplate을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;spring boot 3.x 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplate은 커넥션 풀을 지원하지 않아 구성 요소인 HttpClient를 통해서 커넥션과 관련된 추가 세팅을 진행합니다. 아래 RestTemplateGenerator 클래스는 restTemplate을 만들어내서 반환해 주는 object 클래스로 spring boot 3.x 버전부터 사용하는 RestTemplate 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;object RestTemplateGenerator {

    // https://www.baeldung.com/httpclient-connection-management
    fun generateRestTemplate(
        restTemplateBuilder: RestTemplateBuilder,
        responseTimeout: Int,
        conMaxTotal: Int,
        maxPerRoute: Int,
        conRequestTimeout: Int,
        conTimeout: Int,
        errorHandler: ResponseErrorHandler? = null,
    ): RestTemplate {
        val connManager = connectionManager(conMaxTotal, maxPerRoute, responseTimeout)
        val httpClient = httpClients(connManager)
        val factory = httpComponentsClientHttpRequestFactory(httpClient, conRequestTimeout, conTimeout)
        val restTemplate = restTemplateBuilder.requestFactory(Supplier { factory }).build()
        errorHandler?.let { restTemplate.errorHandler = it }
        return restTemplate
    }

    fun generateFileRestTemplate(
        restTemplateBuilder: RestTemplateBuilder,
        responseTimeout: Int,
        conMaxTotal: Int,
        maxPerRoute: Int,
        conRequestTimeout: Int,
        conTimeout: Int,
        timeToLive: Long,
        errorHandler: ResponseErrorHandler? = null,
    ): RestTemplate {
        val connManager = connectionManager(conMaxTotal, maxPerRoute, responseTimeout, timeToLive)
        val httpClient = httpClients(connManager)
        val factory = httpComponentsClientHttpRequestFactory(httpClient, conRequestTimeout, conTimeout, false)
        val restTemplate = restTemplateBuilder.requestFactory(Supplier { factory })
            .messageConverters(ResourceHttpMessageConverter(true))
            .build()
        errorHandler?.let { restTemplate.errorHandler = it }
        return restTemplate
    }

    private fun connectionManager(
        maxTotal: Int,
        maxPerRoute: Int,
        responseTimeout: Int,
        timeToLive: Long = 10, // 설정하지 않으면 -1
    ): PoolingHttpClientConnectionManager {
        return PoolingHttpClientConnectionManagerBuilder.create()
            .setMaxConnTotal(maxTotal) // 연결 유지할 커넥션 토탈 개수
            .setMaxConnPerRoute(maxPerRoute) // 특정 경로당 커넥션 개수 제한
            .setDefaultSocketConfig(
                SocketConfig.custom() // response timeout
                    .setSoTimeout(responseTimeout, TimeUnit.MILLISECONDS)
                    .build(),
            )
            .setDefaultConnectionConfig(
                // https://stackoverflow.com/questions/39644479/poolingnhttpclientconnectionmanager-what-is-timetolive-attribute-for
                ConnectionConfig.custom() // 커넥션 timeout
                    .setTimeToLive(timeToLive, TimeUnit.SECONDS)
                    .build(),
            )
            .build()
    }

    private fun httpClients(
        poolingHttpClientConnectionManager: PoolingHttpClientConnectionManager,
    ): CloseableHttpClient {
        return HttpClients.custom()
            .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy())
            .setConnectionManager(poolingHttpClientConnectionManager)
            // 연결이 idle 상태로 남아 있을 때 이를 정리하고 메모리를 관리하기 위한 메서드
            // 커넥션 풀에서 일정 시간 동안 사용되지 않은 연결을 제거
            .evictExpiredConnections()

            // 커넥션 풀에서 설정된 최대 수명이 지난 연결을 제거
            // default는 무한이지만, 업로드 중간에 idle 상태가 되어버리면 완료되지 않았음에도 close 될 수 있어서 주석 처리
            // .evictIdleConnections(TimeValue.ofSeconds(80)) 
            .build()
    }

    private fun httpComponentsClientHttpRequestFactory(
        closeableHttpClient: CloseableHttpClient,
        conRequestTimeout: Int,
        conTimeout: Int,
        bufferRequestBody: Boolean = true, // upload, download template 은 false 사용
    ): HttpComponentsClientHttpRequestFactory {
        return HttpComponentsClientHttpRequestFactory(closeableHttpClient).apply {
            setConnectionRequestTimeout(conRequestTimeout)
            setConnectTimeout(conTimeout)
            setBufferRequestBody(bufferRequestBody)
        }
    }
}&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;generateRestTemplate 메서드와 generateFileRestTemplate 메서드의 차이는 ResourceHttpMessageConverter 컨버터 등록과 bufferRequestBody 옵션의 차이입니다. 해당 옵션의 설명은 바로 아래서 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller request로부터 받은 파일 업로드 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun upload(
    param: Param,
    data: InputStream
): String {
    return fileRestTemplate.execute(
        param.uri,
        HttpMethod.PUT,
        {
            it.headers.add(&quot;X-Upload-Content-Length&quot;, param.partLength.toString())
            (it as StreamingHttpOutputMessage).setBody { out -&amp;gt; data.transferTo(out) }
        },
        { it.headers.getFirst(&quot;Etag&quot;) }
    )!!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;execute 메서드는 uri, method, requestCallback, responseExtractor을 인자로 받습니다. requestCallback은 요청을 보내기 전에 값을 세팅해주는 것이고 responseExtractor는 응답을 받아서 추출(조작)하는 것입니다. requestCallback 부분을 조금 더 살펴봅시다.&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;1261&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZJnmp/btsCQoC6IYc/wbjZHEWvJcroiHd1Qmv0XK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZJnmp/btsCQoC6IYc/wbjZHEWvJcroiHd1Qmv0XK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZJnmp/btsCQoC6IYc/wbjZHEWvJcroiHd1Qmv0XK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZJnmp%2FbtsCQoC6IYc%2FwbjZHEWvJcroiHd1Qmv0XK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1261&quot; height=&quot;668&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;equestCallback 부분의 request부터 디버깅을 따라가다 보면 HttpComponentsClientHttpRequestFactory 클래스의 createRequest() 메소드를 호출하고 있습니다. 해당 메서드 코드를 보면 앞전에 restTemplate 설정에서 bufferRequestBody를 false로 두었기 때문에 else 분기를 타게 되면서 HttpComponentsStreamingClientHttpRequest를 생성하게 됩니다.&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;817&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9vfbg/btsCUAWPgg6/jaVgiXyex8CWB0wU0kF6aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9vfbg/btsCUAWPgg6/jaVgiXyex8CWB0wU0kF6aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9vfbg/btsCUAWPgg6/jaVgiXyex8CWB0wU0kF6aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9vfbg%2FbtsCUAWPgg6%2FjaVgiXyex8CWB0wU0kF6aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;817&quot; height=&quot;226&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 여기서 이 false 옵션이 도대체 무엇인지 봅시다. 주석에 의하면 POST, PUT을 통해 대량의 데이터를 전송할 때는 메모리가 부족하지 않도록 해당 속성을 false로 변경하라고 되어있습니다. 기본값인 true로 설정하면 요청 본문을 메모리에 버퍼링 합니다. 이는 요청을 보내기 전에 전체 본문을 메모리에 저장하고, 그 후 HTTP 요청을 보내는 방식입니다. false의 경우 데이터는 메모리에 버퍼링 되지 않고 직접 네트워크를 통해 전송됩니다.&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;1445&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQI4S3/btsCMnrruTH/CYLDtr7CtfF8FSEhHIzTv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQI4S3/btsCMnrruTH/CYLDtr7CtfF8FSEhHIzTv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQI4S3/btsCMnrruTH/CYLDtr7CtfF8FSEhHIzTv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQI4S3%2FbtsCMnrruTH%2FCYLDtr7CtfF8FSEhHIzTv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1445&quot; height=&quot;241&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpComponentsStreamingClientHttpRequest가 반환되므로 이제 requestCallback에 들어오는 request가 어떤 request인지 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXakaL/btsCUZCaFON/1f67yrqtsrblaBWwOqsIL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXakaL/btsCUZCaFON/1f67yrqtsrblaBWwOqsIL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXakaL/btsCUZCaFON/1f67yrqtsrblaBWwOqsIL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXakaL%2FbtsCUZCaFON%2F1f67yrqtsrblaBWwOqsIL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1105&quot; height=&quot;120&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requestCallback쪽을 보면 instaceof로 StreamingHttpOutputMessage로 형변환하는 것이 있는데 HttpComponentsStreamingClientHttpRequest는 StreamingHttpOutputMessage의 구현체이기 때문에 가능합니다. data.transferTo 메서드를 통해 inputstream을 outputStream으로 write 작업 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대용량 다운로드를 위한 restTemplate&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceHttpMessageConverter는 spring에서 제공하는 HttpMessageConverter의 한 종류로, 주로 서버에서 클라이언트로 Resource 타입의 데이터(파일, 이미지)를 전송할 때 사용됩니다. 이 컨버터를 사용하면, 대용량 파일을 스트림 형태로 직접 다운로드할 수 있어, 메모리 사용량을 줄이면서도 대용량 파일을 효율적으로 다룰 수 있습니다. restTemplate은&amp;nbsp; 해당 컨버터의 supportsReadStreaming 옵션이 false로 들어가있기 때문에 추가로 등록해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplate은 응답 타입을 Resource 타입으로 지정했을 때, ResourceHttpMessageConverter를 사용하여 응답을 처리하게 됩니다. 이때 바이트 배열을 사용하지 않고 스트림 통신을 사용하게 됩니다.(메모리를 사용하지 않고 통로를 열어둔 것)&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
protected Resource readInternal(Class&amp;lt;? extends Resource&amp;gt; clazz, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {

    if (this.supportsReadStreaming &amp;amp;&amp;amp; InputStreamResource.class == clazz) {
        return new InputStreamResource(inputMessage.getBody()) {
            @Override
            public String getFilename() {
                return inputMessage.getHeaders().getContentDisposition().getFilename();
            }
            @Override
            public long contentLength() throws IOException {
                long length = inputMessage.getHeaders().getContentLength();
                return (length != -1 ? length : super.contentLength());
            }
        };
    }
    else if (Resource.class == clazz || ByteArrayResource.class.isAssignableFrom(clazz)) {
        byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
        return new ByteArrayResource(body) {
            @Override
            @Nullable
            public String getFilename() {
                return inputMessage.getHeaders().getContentDisposition().getFilename();
            }
        };
    }
    else {
        throw new HttpMessageNotReadableException(&quot;Unsupported resource class: &quot; + clazz, inputMessage);
    }
}&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;내부적으로 코드를 까보면, ResourceHttpMessageConverter를 등록하게 되면 supportsReadStreaming값이 true가 되면서 stream 통신을 하게 되는 것을 확인할 수 있습니다. 따라서 메모리에 올라가지 않아도 스트림으로 통신을 하고 있기 때문에 이 통로가 끊기면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;protected &amp;lt;T&amp;gt; T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor&amp;lt;T&amp;gt; responseExtractor) throws RestClientException {

    Assert.notNull(url, &quot;url is required&quot;);
    Assert.notNull(method, &quot;HttpMethod is required&quot;);
    ClientHttpRequest request;
    try {
        request = createRequest(url, method);
    }
    catch (IOException ex) {
        ResourceAccessException exception = createResourceAccessException(url, method, ex);
        throw exception;
    }
    ClientRequestObservationContext observationContext = new ClientRequestObservationContext(request);
    observationContext.setUriTemplate(uriTemplate);
    Observation observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(this.observationConvention,
            DEFAULT_OBSERVATION_CONVENTION, () -&amp;gt; observationContext, this.observationRegistry).start();
    ClientHttpResponse response = null;
    try {
        if (requestCallback != null) {
            requestCallback.doWithRequest(request);
        }
        response = request.execute();
        observationContext.setResponse(response);
        handleResponse(url, method, response);
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }
    catch (IOException ex) {
        ResourceAccessException exception = createResourceAccessException(url, method, ex);
        observation.error(exception);
        throw exception;
    }
    catch (RestClientException exc) {
        observation.error(exc);
        throw exc;
    }
    finally {
        if (response != null) {
            response.close();
        }
        observation.stop();
    }
}&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;위 코드는 원본 restTemplate의 doExecute 코드입니다. 마지막에 finally 구문으로 인해 Stream으로 응답값을 처리하는 경우 문제가 발생합니다. RestTemplate의 리턴타입을 InputStreamResource으로 설정해두고 로직에서 해당 stream을 사용하려고 하면 &lt;b&gt;stream already closed &lt;/b&gt;예외가 발생합니다. 왜냐하면 doExecute가 완료되는 순간 finally 블록에 의해 스트림이 닫히기 때문입니다. 이는 스트림을 통해 데이터를 계속 읽거나 쓰려는 작업에 영향을 줄 수 있으며, 데이터가 완전히 전송되기 전에 끊어져 데이터 손실이 발생합니다. 이를 해결하기 위해서는 ResponseExtractor를 활용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class DownloadSample(
    val restTemplate: RestTemplate,
) {
    fun getObject(command: GetObjectCommand): String {
        return runCatching {
            command.outputStream.use { os -&amp;gt;
                restTemplate.execute(
                    UriComponentsBuilder.fromHttpUrl(&quot;http://localhost:8080&quot;).build().toUri(),
                    HttpMethod.GET,
                    {},
                    {
                        it.body.transferTo(os)
                        String(Base64.getDecoder().decode(it.headers.getFirst(&quot;x-fileName&quot;)))
                    },
                )
            }
        }.onFailure {
            throw RuntimeException(&quot;&quot;)
        }.getOrNull()!!
    }
}&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;인자로 outputStream을 받아와서 그곳으로 stream을 연결해주면 됩니다. controller의 response로 바로 내보내고 싶다면 response의 outputStream으로 연결해주면 되고 내부에서 다시 사용해야 한다면 임시 파일을 하나 생성하여 FileOutputStream으로 연결해주고 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;restTemplate 등록 시 주의사항&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cthYj3/btsCQphHPL8/hNLHdlK3lVE3nIOwc4z1zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cthYj3/btsCQphHPL8/hNLHdlK3lVE3nIOwc4z1zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cthYj3/btsCQphHPL8/hNLHdlK3lVE3nIOwc4z1zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcthYj3%2FbtsCQphHPL8%2FhNLHdlK3lVE3nIOwc4z1zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1352&quot; height=&quot;816&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;RestTemplateGenerator object 클래스의 메서드를 통해 restTemplate을 별도로 만들어서 사용하다가 문제가 발생했습니다. 성능 테스트 결과 thread count가 13,000까지 올라갔다가 서버가 죽는 현상이 발생했습니다. thread dump 결과 &lt;b&gt;idle-connection-evictor-1&lt;/b&gt; 이름을 가진 스레드가 계속 생성되는 것을 확인할 수 있었습니다. 스레드의 이름으로 보았을 때, restTemplate의 evictIdleConnections, evictExpiredConnections 설정을 의심해 볼 수 있었습니다. 해당 메서드에는 다음과 같은 설명이 포함되어 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;One MUST explicitly close HttpClient with CloseableHttpClient.close() in order to stop and release the background thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드 스레드를 중지하고 해제하려면 CloseableHttpClient.close()를 사용하여 HttpClient를 명시적으로 닫아야 한다고 합니다. 그래서 일단 옵션을 제거해 보니 evict 관련 스레드가 아예 생성되지 않아서 스레드가 증가하지 않는 것을 확인할 수 있었습니다. 이후에 다시 옵션을 추가하고 restTemplate을 사용하는 곳에서 finally를 통해 httpClient.close()를 호출하니 evict 관련 스레드는 새로 생성되었다가 close 호출 후 사라지는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, restTemplate은 커넥션 풀을 지원하지 않아 구성 요소인 HttpClient를 통해서 이를 보완하는데 HttpClient에서 사용한 evictIdleConnections, evictExpiredConnections 옵션으로 인해 문제가 발생한 것입니다. restTemplate을 사용하는 곳마다 별도로 생성해서 사용하고 있는데 과정에서 HttpClient가 계속적으로 생성되다 보니 무거워질 수밖에 없었던 것이 문제였습니다. 따라서 이를 위해 restTemplate을 지속적으로 생성하는 것이 아닌 빈등록으로 사용하여 해결할 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Bean
fun fileRestTemplateTimeoutOneMinute(): RestTemplate {
    return generateRestTemplate(
        restTemplateBuilder = restTemplateBuilder,
        responseTimeout = 60000,
        conMaxTotal = 15,
        maxPerRoute = 15,
        conRequestTimeout = 3000,
        conTimeout = 3000,
        errorHandler = ...,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-resttemplate-download-large-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Download&amp;nbsp;a&amp;nbsp;Large&amp;nbsp;File&amp;nbsp;Through&amp;nbsp;a&amp;nbsp;Spring&amp;nbsp;RestTemplate&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>download</category>
      <category>File</category>
      <category>resttemplate</category>
      <category>spring</category>
      <category>upload</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/57</guid>
      <comments>https://backtony.tistory.com/57#entry57comment</comments>
      <pubDate>Sun, 31 Dec 2023 13:46:49 +0900</pubDate>
    </item>
    <item>
      <title>JPA - AttributeConverter</title>
      <link>https://backtony.tistory.com/56</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;AttributeConverter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AttributeConverter는 주로 다음과 같은 상황에서 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA가 지원하지 않는 타입을 매핑&lt;/li&gt;
&lt;li&gt;두 개 이상의 속성을 갖는 밸류 타입을 한 개 칼럼에 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPA가 지원하지 않는 타입을 매핑&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface AttributeConverter&amp;lt;X,Y&amp;gt; {
    public Y convertToDatabaseColumn (X attribute);
    public X convertToEntityAttribute (Y dbData);
}&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;X : 엔티티의 속성에 대응하는 타입&lt;/li&gt;
&lt;li&gt;Y : DB에 대응하는 타입&lt;/li&gt;
&lt;li&gt;convertToDatabaseColumn
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티의 X 타입 속성을 Y 타입의 DB 데이터로 변환합니다.&lt;/li&gt;
&lt;li&gt;엔티티 속성을 DB에 반영할 때 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;convertToEntityAttribute
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Y 타입으로 읽은 DB 데이터를 엔티티의 X 타입의 속성으로 변환합니다.&lt;/li&gt;
&lt;li&gt;엔티티 조회시 DB에서 읽어온 데이터를 엔티티의 속성에 반영할 떄 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public enum DirectoryType {
    DEFAULT, CUSTOM, UNKNOWN
}

@Converter
public class DirectoryTypeConverter implements AttributeConverter&amp;lt;DirectoryType, String&amp;gt; {
    @Override
    public String convertToDatabaseColumn(DirectoryType attribute) {
        return attribute.name();
    }

    @Override
    public DirectoryType convertToEntityAttribute(String dbData) {
        return Arrays.stream(DirectoryType.values()).filter(constraintSet -&amp;gt; constraintSet.name().equals(dbData))
            .findAny().orElse(DirectoryType.UNKNOWN);
    }
}

@Entity
public class ServiceDirectory {
    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long no;


    @Convert(converter = DirectoryTypeConverter.class)
    private DirectoryType directoryType;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 개 이상의 속성을 갖는 밸류 타입을 한 개 컬럼에 매핑&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우는 흔치 않지만 이렇게 할 수 있다 정도로만 알아두면 될 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class Money {
    private Double value;
    private String currency;

    public Money(Double value, String currency) {
        this.value = value;
        this.currency = currency;
    }

    public Double getValue() {
        return value;
    }

    public void setValue(Double value) {
        this.value = value;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }

    @Override
    public String toString() {
        return value.toString() + currency;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Money 타입을 DB에 보관할 때 &amp;ldquo;1000KRW&amp;rdquo;이나 &amp;ldquo;100USD&amp;rdquo;와 같은 문자열로 저장다고 가정합니다.&lt;/li&gt;
&lt;li&gt;Money를 한 개의 칼럼에 매핑하므로 @Embeddable을 사용할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Converter(autoApply = true)
public class MoneyConverter implements AttributeConverter&amp;lt;Money, String&amp;gt; {

    @Override
    public String convertToDatabaseColumn(Money attribute) {
        if (attribute == null) {
            return null;
        }
        return attribute.toString();
    }

    @Override
    public Money convertToEntityAttribute(String dbData) {
        if (dbData == null) {
            return null;
        } else {
            String value = dbData.substring(0, dbData.length() - 3);
            String currency = dbData.substring(dbData.length() - 3);
            return new Money(Double.valueOf(value), currency);
        }
    }
}&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;autuApply 설정을 true로 설정했으므로 JPA 프로바이더는 MoneyConverter를 자동으로 적용합니다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>AttributeConverter</category>
      <category>JPA</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/56</guid>
      <comments>https://backtony.tistory.com/56#entry56comment</comments>
      <pubDate>Sun, 31 Dec 2023 11:35:37 +0900</pubDate>
    </item>
    <item>
      <title>Tomcat의 file upload (feat. octet-stream, multipart)</title>
      <link>https://backtony.tistory.com/55</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;application/octet-stream은 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링 결과는 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application/octet-stream은 이 타입은 이진 파일을 위한 기본값입니다. 이 타입은 실제로 잘 알려지지 않은 이진 파일을 의미하므로, 브라우저는 보통 자동으로 실행하지 않거나 실행해야 할지 묻기도 합니다. Content-Disposition 헤더가 값 attachment 와 함게 설정되었고 'Save As' 파일을 제안하는지 여부에 따라 브라우저가 그것을 다루게 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application/octet-stream은 알려지지 않은 이진 파일을 알리는 content-type이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;multipart/form-data&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 HTTP Request의 Body에 전송하고자 하는 데이터를 넣을 수 있습니다. Body에 들어가는 데이터의 타입을 HTTP Header에 Content-type으로 명시해 줌으로써 서버가 타입에 알맞게 처리하도록 유도할 수 있습니다. 보통 HTTP Request의 Body는 한 종류의 타입이 대부분이고, 그에 따라 Content-type도 타입을 하나만 명시할 수 있습니다. 예를 들면, text이면 text/plain, xml이면 text/xml, jpg이미지면 image/jpeg 입니다. 일반적인 form의 submit에 의한 데이터들의 Content-type은 application/x-www-form-urlencoded 입니다. 하지만 사진 파일 업로드를 예로 들면, 사진 설명에 대한 input과 사진 파일을 위한 input 2개가 들어갑니다. 이 경우에는 두 input 간에 Content-type이 전혀 다릅니다. 사진 설명은 application/x-www-form-urlencoded 이고, 사진 파일은 image/jpeg 입니다. 두 종류의 데이터가 하나의 HTTP Request Body에 들어가야 하는데, 한 Body에서 두 종류의 데이터를 구분에서 넣어주는 방법이 필요해지면서 multipart 타입이 등장하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU9wqq/btsCTVUmEeo/mZD0rdL3fYGs5ELneNxkDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU9wqq/btsCTVUmEeo/mZD0rdL3fYGs5ELneNxkDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU9wqq/btsCTVUmEeo/mZD0rdL3fYGs5ELneNxkDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU9wqq%2FbtsCTVUmEeo%2FmZD0rdL3fYGs5ELneNxkDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;350&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 multipart/form-data 요청에 대한 HTTP 메시지입니다. HTTP Header 부분은 Boundary가 추가된 것을 제외하면 일반적인 다른 Content-Type과 다른것이 없습니다. 반면에 Payload 부분은 다른 Content-Type의 Payload와 비교했을 때 다른 부분이 존재합니다. Payload를 보면 Boundary를 통해 content-type이 구분되어 있는 것이 보입니다. First Boundary를 보면 ContentType이 image/png인 것이 보이고, 두 번째 data 부분에는 파일이 아니라 일반 데이터인 것이 보입니다. &lt;b&gt;즉, multipart/form-data는 하나의 요청에 여러 content-type을 담는 방식을 제공하는 방식입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Tomcat의 파일 업로드 구현 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조적 차이를 설명했고, tomcat에서는 이 두가지 content-type을 사용할 때, file을 처리하는 방식에 차이가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bverip/btsCTWyUCAx/Kq8kNeNkDKzPCytg76F2kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bverip/btsCTWyUCAx/Kq8kNeNkDKzPCytg76F2kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bverip/btsCTWyUCAx/Kq8kNeNkDKzPCytg76F2kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbverip%2FbtsCTWyUCAx%2FKq8kNeNkDKzPCytg76F2kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;811&quot; height=&quot;423&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;423&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat을 통해 데이터를 전송하게 되면 다음과 같은 방식으로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Client는 Socket Send Buffer로 데이터를 보낸다.&lt;/li&gt;
&lt;li&gt;Socket Send Buffer가 가득차면 Socket Receive Buffer로 보낸다.&lt;/li&gt;
&lt;li&gt;Tomcat ReadBuf는 Socket Receive Buffer에 있는 것을 InputStream으로 감싸서 읽어온다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;감싸는 이유는 readbuf가 socketbuf로부터 읽어서 어플리케이션에 제공하는 일련의 과정을 추상화 하기 위함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 톰켓은 데이터 전송 요청이 오면 Payload에 해당하는 데이터를 한번에 읽어 메모리에 올리지 않습니다. Socket Recieve Buffer를 읽어올 때 InputStream으로 감싸서 읽어오기 때문에 우리는 Controller의 request에서 getInputStream을 사용하여 Socket Receive Buffer를 읽어서 파일을 읽어올 수 있는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 데이터 전송은 위와 같이 동작하지만 톰캣은 Content-Type이 multipart/form-data일 때는 다르게 동작합니다. 톰캣은 multipart/form-data 타입의 payload 데이터를 메모리에 로딩하지 않고 &lt;b&gt;서버의 디스크에 임시로 저장하고 나중에 다시 삭제합니다.&lt;/b&gt; 디스크에 저장된 것을 읽어와야하기 때문에 효율성이 낮을 수밖에 없습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그렇다면 왜 이런 방법을 택했을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdYhiY/btsCLrne52t/iqKRAOYlCcJrpLkgbJcc51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdYhiY/btsCLrne52t/iqKRAOYlCcJrpLkgbJcc51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdYhiY/btsCLrne52t/iqKRAOYlCcJrpLkgbJcc51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdYhiY%2FbtsCLrne52t%2FiqKRAOYlCcJrpLkgbJcc51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;805&quot; height=&quot;412&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCP Blocking 때문입니다.&lt;/b&gt; 만약 Disk에 저장되지 않는다면, 클라이언트와 서버가 데이터를 보내고 받는 작업이 계속 진행되게 됩니다. 그런데 서버에서 Socket Receive Buffer가 가득찼는데도 데이터를 읽어가지 않는다면 socket receive buffer가 비워지지 않기 때문에 Socket Send Buffer에서도 socker receive buffer로 데이터를 보낼 수 없게 되고 전체에서 병목 현상이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 일반적으로 File은 Socket Send Buffer, Socket Receive Buffer의 사이즈보다 큽니다. 따라서 Socket receive Buffer를 사용하게 되면 TCP Connection을 맺게 되면서 TCP Blocking 발생할 수 있으므로 이를 막고자 Payload 부분을 한번에 읽어서 디스크에 저장하는 방식을 선택하고 있는 것입니다. 덕분에 서버는 톰캣에서 디스크에 저장해놓은 것을 읽어오면 되기 때문에 Socket Recieve Buffer가 빨리 비워지지 않아서 TCP Blocking이 일어나는 것을 막을 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 다르겠지만 네트워크 환경이 안좋은 곳이라면 디스크에 저장한 후 읽어오는 방식이 더 좋은 방식입니다. 반면에 한국처럼 네트워크가 빠르다면 TCP Blocking 시간 자체가 없다고 봐도 되기 때문에 디스크에 파일을 저장하고 다시 읽어오는 작업은 비효율적입니다. 더불어, 해당 파일이 큰 경우라면, 디스크에 i/o를 하는 작업이 성능에 영향을 줄 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, tomcat에서 데이터를 모두 읽어서 임시로 파일로 저장해두느냐, 아니면 읽는대로 바로 처리하는 것을 반복하느냐 정도로 octect-stream 방식과 multipart 방식을 구분해서 사용할 수 있다는 것입니다.&lt;/p&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론적으로 네트워크 상황이 좋다면, application/octect-stream이 tomcat에서 파일을 처리하기에 용이합니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 신경쓸게 줄어들기 때문입니다. (디스크 공간, 버퍼 크기 등) 신경쓸게 줄어들고 TCP Blocking이 별다른 성능 저하를 일으키지 않는다면 application/octect-stream을 선택하지 않을 이유가 없습니다. 네트워크 상황이 좋지 않아도 버퍼 크기를 충분히 키운다면 마찬가지로 TCP Blocking이 별다른 성능 저하를 일으키지 않게 됩니다. 좀 더 심화해보자면, 네트워크 상황은 구간별로 봐야합니다. 왜냐면 보통 서버를 단일 레이어로 구성하지 않기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 클라이언트와 리버스 프록시 서버 사이는 대체로 네트워크 상황이 좋지 못하고, 리버스 프록시 서버와 백엔드 서버 사이는 네트워크 상황이 굉장히 좋습니다. 그렇다면 리버스 프록시 서버의 버퍼를 키워서 네트워크 이슈(Blocking)를 회피하고 백엔드로 보낼때는 별다른 고민 없이 보내는 방안을 선택할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 왜 이렇게 하느냐면, 보통 리버스 프록시가 설정이 간단하고 단순한 작업을 처리하는데 성능이 더 좋기 때문입니다. 게다가 스케일아웃할때 비용도 생각해봐야하는데, 버퍼 역할만 스케일아웃 하면 되는데 비싼 JVM 서버를 올리는건 비용적으로 좋지 못한 선택입니다. tomcat을 띄우려면 jdk가 필요하고 많은 메모리가 필요하고 또 많은 코드가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 상황이 좋다면 application/octect-stream 방식은 프론트에서는 파일을 청크단위로 쪼개서 굳이 나눠서 올릴 필요는 없습니다. 하지만 상황이 좋지 않다면 이 방식 또한 청크 단위로 쪼개서 청크 크기만큼의 버퍼를 제공해야 합니다. 청크 단위로 나누는 이유는 무한한 크기의 버퍼를 제공할 수는 없기 때문입니다. octect 방식은 쪼갤수도 안쪼갤수도 있지만 multipart 방식은 무조건 청크로 나눠야 합니다. 왜냐하면 tomcat에서 multipart를 스트리밍해서 받는 옵션을 제공하지 않기 때문입니다. octect-stream으로 업로드를 시도하면 스레드 하나가 점유되기 때문에 업로드에 스레드들이 모두 점유되버리면 응답지연이 발생할 수 있기 때문에 큰 경우라면 octect도 쪼개서 진행하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크로 나누는 이유를 정리해보자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버퍼 크기에 맞추려고&lt;/li&gt;
&lt;li&gt;병렬 업로드 하려고&lt;/li&gt;
&lt;li&gt;1개 세션이 너무 길어지는 것을 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국에서 청크를 나누지 않아도 되는 이유는 버퍼나 병렬이 필요 없을 정도로 네트워크가 빨라서입니다. 한국일지라도 3번때문에 청크로 나누는게 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 정리해보면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적당한 크기 + form-data 들은 multipart/form-date 방식을 사용&lt;/li&gt;
&lt;li&gt;본격화된 업로드 기능(대규모 업로드)은 application/octect-stream&lt;/li&gt;
&lt;li&gt;파일 크기가 어느 정도 크다면 청크 단위로 나눠서 전송&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>ETC</category>
      <category>file upload</category>
      <category>multipart</category>
      <category>octect-stream</category>
      <category>Tomcat</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/55</guid>
      <comments>https://backtony.tistory.com/55#entry55comment</comments>
      <pubDate>Sun, 31 Dec 2023 11:20:37 +0900</pubDate>
    </item>
    <item>
      <title>가비지 컬렉터</title>
      <link>https://backtony.tistory.com/54</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 구조&lt;/h2&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;443&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brxFkQ/btsCTBuQMes/JR0arFyYAFOINtICBn29tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brxFkQ/btsCTBuQMes/JR0arFyYAFOINtICBn29tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brxFkQ/btsCTBuQMes/JR0arFyYAFOINtICBn29tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrxFkQ%2FbtsCTBuQMes%2FJR0arFyYAFOINtICBn29tK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;443&quot; height=&quot;741&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로그램은 메모리에 올라와야 실행할 수 있습니다. 따라서 프로그램에 사용되는 변수들을 저장할 메모리가 필요한데 운영체제는 프로그램의 실행을 위해 다양한 메모리 공간을 제공합니다. 대표적으로 위와 같은 4가지 영역이 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Code 영역
&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;CPU는 코드 영역에서 저장된 명령어를 하나씩 가져가서 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Data 영역
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전역 변수, 정적 변수가 저장되는 영역입니다.&lt;/li&gt;
&lt;li&gt;프로그램의 시작과 함께 할당되어 프로그램이 종료되면 소멸합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Heap 영역
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로그램을 실행하면서 생성한 모든 객체가 저장되는 영역입니다.(흔히 new를 통해 성상한 모든 Object 타입의 인스턴스가 저장되는 영역)&lt;/li&gt;
&lt;li&gt;힙 영역에 보관되는 메모리는 메소드 호출이 끝나도 사라지지 않고 유지되다가 이것을 &lt;b&gt;JVM의 가비지 컬렉터&lt;/b&gt; 가 메모리 해제하여 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stack 영역
&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;힙 영역에 생성된 Object 타입의 데이터의 참조값을 할당합니다.&lt;/li&gt;
&lt;li&gt;함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸합니다.&lt;/li&gt;
&lt;li&gt;컴파일 타임에 크기가 결정됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JVM의 메모리 구조&lt;/h3&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;997&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3XbZS/btsCSbDfIVk/qJCPsd3pJECwOUdTLpqKoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3XbZS/btsCSbDfIVk/qJCPsd3pJECwOUdTLpqKoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3XbZS/btsCSbDfIVk/qJCPsd3pJECwOUdTLpqKoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3XbZS%2FbtsCSbDfIVk%2FqJCPsd3pJECwOUdTLpqKoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;997&quot; height=&quot;298&quot; data-origin-width=&quot;997&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;JVM은 OS로부터 메모리르 할당 받은 후 메모리를 용도에 따라 여러 영역으로 나누어서 관리합니다. 이는 크게 두 영역으로 나눌 수 있습니다. 모든 쓰레드가 공유하는 영역으로 Method Area와 Heap 영역이 있고, 각 쓰레드마다 고유하게 생성하며 쓰레드 종료시 소멸되는 Stack, Pc Register, Native Method Stack 영역이 있습니다. 간단하게 살펴보면 다음과 같습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Method Area( = Class Area, Static Area)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 파일의 바이트 코드가 로드되는 곳입니다.&lt;/li&gt;
&lt;li&gt;메인 메서드에서 사용하는 &lt;b&gt;클래스와 static 변수&lt;/b&gt; 가 메서드 영역에 저장됩니다.&lt;/li&gt;
&lt;li&gt;프로그램의 클래스 구조를 메타 데이터처럼 가지고 있고 메서드의 코드를 저장해둡니다.&lt;/li&gt;
&lt;li&gt;메서드 영역에 코드가 올라가는 것을 클래스 로딩이라고 하는데 메서드가 호출되려면 해당 메서드를 갖고 있는 클래스 파일이 메모리에 로딩되어 있어야하기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Heap
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 실행 중에 생성되는 객체 인스턴스를 저장하는 영역으로 &lt;b&gt;JVM GC에 의해 관리&lt;/b&gt; 되는 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stack
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 호출을 스택 프레임이라는 블록으로 쌓으며 로컬 변수, 중간 연산 결과들이 저장되는 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;pc register
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰레드가 현재 실행할 스택 프레임의 주소를 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Native Method Stack
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C/C++ 등의 Low level 코드를 실행하는 스택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가비지 컬렉터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가비지 컬렉터는 &lt;b&gt;동적으로 할당한 메로리 영역 중 사용하지 않는 영역을 탐지하여 해제하는 역할&lt;/b&gt; 을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1926&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzxHMh/btsCN4yePzn/AcfXSm7KAMkSKsWpYzp4C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzxHMh/btsCN4yePzn/AcfXSm7KAMkSKsWpYzp4C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzxHMh/btsCN4yePzn/AcfXSm7KAMkSKsWpYzp4C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzxHMh%2FbtsCN4yePzn%2FAcfXSm7KAMkSKsWpYzp4C1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1926&quot; height=&quot;672&quot; data-origin-width=&quot;1926&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메인 메서드가 실행되면 스택에 num1, num2, sum 값이 쌓이게 되고, name은 Heap 영역에 쌓이고 스택에서는 이를 참조합니다. 메인 메서드가 끝나게 되면 스택이 전부 pop되고 Heap 영역에 객체 타입의 데이터만 남게 됩니다. 이런 객체를 &lt;b&gt;Unreachable Object&lt;/b&gt; 라고 표현하고 &lt;b&gt;가비지 컬렉터의 대상&lt;/b&gt; 이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가비지 컬렉터의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 누수 방지&lt;/li&gt;
&lt;li&gt;해제된 메모리 접근 방지&lt;/li&gt;
&lt;li&gt;해제한 메모리를 다시 이중 해제하는 것 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 언제 GC가 메모리를 해제하는지 모름&lt;/li&gt;
&lt;li&gt;실행중인 애플리케이션이 리소스를 GC 작업에 내줘야 하므로 오버헤드 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GC 알고리즘&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference Counting&lt;/h3&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;944&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blQjhU/btsCTwAo0qS/HJyCZ6wlYIVchSgcHfKMWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blQjhU/btsCTwAo0qS/HJyCZ6wlYIVchSgcHfKMWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blQjhU/btsCTwAo0qS/HJyCZ6wlYIVchSgcHfKMWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblQjhU%2FbtsCTwAo0qS%2FHJyCZ6wlYIVchSgcHfKMWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;944&quot; height=&quot;301&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;reference count는 몇 가지 방법으로 해당 객체에 접근할 수 있는지를 의마합니다. 해당 객체에 접근할 수 있는 방법이 없다면, reference count가 0이 되면 가비지 컬렉션의 대상이 됩니다. 하지만 이 알고리즘은 순환참조의 문제가 발생합니다. Root Space에서 Heap space 접근을 모두 끊는다고 가정하면, 오른쪽 그림의 노란색 부분은 서로가 서로를 참조하고 있기 때문에 reference count가 1로 유지되면서 사용하지 않는 메모리 영역이 해제되지 못하고 메모리 누수가 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mark And Sweep&lt;/h3&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;1098&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSZ6U4/btsCP5XOQD3/C0q1KVeKlQxKlRBDe3WYD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSZ6U4/btsCP5XOQD3/C0q1KVeKlQxKlRBDe3WYD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSZ6U4/btsCP5XOQD3/C0q1KVeKlQxKlRBDe3WYD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSZ6U4%2FbtsCP5XOQD3%2FC0q1KVeKlQxKlRBDe3WYD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1098&quot; height=&quot;331&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;root space 에서부터 해당 객체에 접근 가능한지를 해제의 기준으로 합니다. 루트부터 그래프 순회를 통해 연결된 객체를 찾아내고, 연결이 끊어진 객체를 지우는 방식입니다. 루트로부터 연결된 객체를 &lt;b&gt;Reachable&lt;/b&gt;, 연결되지 않았다면 &lt;b&gt;Unreachable&lt;/b&gt; 이라고 합니다. 위의 오른쪽 그림에서는 Sweep이후에 분산되었던 메모리가 정리된 것을 확인할 수 있는데 이를 메모리 파편화를 막는 Compaction이라고 합니다. 다만, 이 과정은 필수가 아닙니다. &lt;b&gt;자바는 Mark And Sweep 방식으로 CG를 진행합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p 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;가비지 컬렉터가 Stack의 모든 변수를 스캔하면서 각각 어떤 힙에 있는 객체를 참조하고 있는지 찾아서 마킹합니다. (Mark 과정)&lt;/li&gt;
&lt;li&gt;마킹하고 있는 객체가 참조하고 있는 객체 또한 찾아서 마킹합니다. (Mark 과정)&lt;/li&gt;
&lt;li&gt;마킹되지 않는 객체를 Heap에서 제거합니다. (Sweep 과정)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;의도적으로 GC를 실행시켜줘야 한다.&lt;/li&gt;
&lt;li&gt;애플리케이션 실행과 GC 실행이 병행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 어느 순간에는 실행중인 애플리케이션이 GC에게 리소스를 내줘야 한다는 것입니다.&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;root space&lt;/h4&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;669&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Du0Ss/btsCLqu0vfj/nyRxcaiykld8c5yskaXdV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Du0Ss/btsCLqu0vfj/nyRxcaiykld8c5yskaXdV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Du0Ss/btsCLqu0vfj/nyRxcaiykld8c5yskaXdV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDu0Ss%2FbtsCLqu0vfj%2FnyRxcaiykld8c5yskaXdV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;361&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;포스팅의 제일 첫 부분에서 JVM의 메모리를 설명했습니다. 기억이 안나신다면 다시 맨위로 돌아가서 JVM 메모리 구조를 보시고 오면 됩니다. 위 그림에서 노란색 영역으로 표시된 부분이 GC의 시작점 Root Space에 해당하는 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언제 수행되는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1062&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brMIHM/btsCUyEBI1P/e6ZDZ7KFssUwvixb5g93z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brMIHM/btsCUyEBI1P/e6ZDZ7KFssUwvixb5g93z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brMIHM/btsCUyEBI1P/e6ZDZ7KFssUwvixb5g93z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrMIHM%2FbtsCUyEBI1P%2Fe6ZDZ7KFssUwvixb5g93z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1062&quot; height=&quot;374&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 그림은 JVM의 Heap 영역 구성입니다. JVM의 Heap 영역은 크게 &lt;b&gt;Young Generation과 Old Generation&lt;/b&gt; 으로 나뉩니다. Young Generation에서 발생하는 GC는 &lt;b&gt;Minor GC&lt;/b&gt; , Old Generation에서 발생하는 GC는 &lt;b&gt;Major GC&lt;/b&gt; 라고 부릅니다. young과 old가 모두 꽉차면 Full GC(minor GC + Major GC)가 발생합니다. Young Generation은 &lt;b&gt;Eden, Survival 0, Survival 1&lt;/b&gt; 영역으로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Eden
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새롭게 생성된 객체들이 할당되는 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Servival 0, 1
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Minor GC로부터 살아남은 객체들이 존재하는 영역&lt;/li&gt;
&lt;li&gt;0 또는 1 둘중 하나는 반드시 비어있어햐 합니다.&lt;/li&gt;
&lt;li&gt;둘로 나눠져 있는 이유는 &lt;b&gt;메모리의 단편화&lt;/b&gt; 를 막기 위해서 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성을 살펴보았으니 이제 어떻게 언제 수행되고 어떻게 동작하는지 순서대로 보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1829&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH8e2C/btsCQsrQ3Nk/k5sershDrQo2eWqjpMvxmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH8e2C/btsCQsrQ3Nk/k5sershDrQo2eWqjpMvxmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH8e2C/btsCQsrQ3Nk/k5sershDrQo2eWqjpMvxmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH8e2C%2FbtsCQsrQ3Nk%2Fk5sershDrQo2eWqjpMvxmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1829&quot; height=&quot;443&quot; data-origin-width=&quot;1829&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 객체가 계속 생성되다가 Eden 영역이 꽉차는 순간 Minor GC가 발생합니다. mark and sweep이 진행되고 Unreachable은 해제되고 Reachable이라고 판단되는 객체들은 Survival 0 역역으로 옮겨지면서 age-bit가 0에서 1로 증가합니다. age-bit는 Minor GC에서 살아남을 때마다 1씩 증가합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E9Ec0/btsCRfMxAwh/QFa2A3eKp0BE5RzYVHqEUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E9Ec0/btsCRfMxAwh/QFa2A3eKp0BE5RzYVHqEUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E9Ec0/btsCRfMxAwh/QFa2A3eKp0BE5RzYVHqEUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE9Ec0%2FbtsCRfMxAwh%2FQFa2A3eKp0BE5RzYVHqEUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1599&quot; height=&quot;298&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 지나 Eden 영역이 꽉차게 되면 다시 Minor GC가 발생합니다. 이번에는 Reachable이라고 판단된 객체들이 Survival 1 영역으로 이동합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1611&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0AvWk/btsCSCtY7W2/iITqBGd5fKA9ZkPXs4mOi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0AvWk/btsCSCtY7W2/iITqBGd5fKA9ZkPXs4mOi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0AvWk/btsCSCtY7W2/iITqBGd5fKA9ZkPXs4mOi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0AvWk%2FbtsCSCtY7W2%2FiITqBGd5fKA9ZkPXs4mOi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1611&quot; height=&quot;299&quot; data-origin-width=&quot;1611&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 지나 Eden 영역이 꽉차게 되면 다시 Minor GC가 발생합니다. 이번에는 Reachable이라고 판단된 객체들이 Survival 0 영역으로 이동합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1093&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LmK2N/btsCQpaRByq/vOkAnpUMgkGesdbuz0kRKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LmK2N/btsCQpaRByq/vOkAnpUMgkGesdbuz0kRKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LmK2N/btsCQpaRByq/vOkAnpUMgkGesdbuz0kRKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLmK2N%2FbtsCQpaRByq%2FvOkAnpUMgkGesdbuz0kRKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1093&quot; height=&quot;384&quot; data-origin-width=&quot;1093&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM GC에서는 일정 수준의 age-bit를 넘어가면 오래도록 참조될 객체라고 판단하여 해당 객체를 Old Generation으로 넘겨주는데 이 과정을 &lt;b&gt;Promotion&lt;/b&gt; 이라고 합니다. java8 Parallel GC 사용 기준 age-bit가 15가 되면 promotion이 진행됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1063&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHLwpv/btsCQrT1TiJ/kmNU7Tkl1LIKuPzEN255Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHLwpv/btsCQrT1TiJ/kmNU7Tkl1LIKuPzEN255Q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHLwpv/btsCQrT1TiJ/kmNU7Tkl1LIKuPzEN255Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHLwpv%2FbtsCQrT1TiJ%2FkmNU7Tkl1LIKuPzEN255Q1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1063&quot; height=&quot;371&quot; data-origin-width=&quot;1063&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언젠가 Old Generation 영역이 꽉차게되면 이때는 &lt;b&gt;Major GC&lt;/b&gt; 가 발생합니다. Mark And Sweep 방식을 통해 필요없는 메모리를 비워줍니다. Major GC는 Minor GC보다 더 오래 걸립니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Young Generation과 Old Generation&lt;/h3&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;605&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xkdbw/btsCK9mBGwT/4LI1sdn2yRa0Vjq0sycdjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xkdbw/btsCK9mBGwT/4LI1sdn2yRa0Vjq0sycdjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xkdbw/btsCK9mBGwT/4LI1sdn2yRa0Vjq0sycdjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXkdbw%2FbtsCK9mBGwT%2F4LI1sdn2yRa0Vjq0sycdjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;412&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;heap 영역을 두 영역으로 나눈데는 이유가 있습니다. GC 개발자들이 애플리케이션을 분석해보니 대부분의 객체들의 수명이 짧다는 것을 확인했습니다. GC도 결국 비용이므로 메모리의 특정 부분만을 탐색하여 해제하면 효율적이기 때문에 Young Generation에서 최대한 처리하도록 나눴다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 애플리케이션과 병행되는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;Stop The World
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것을 의미합니다.&lt;/li&gt;
&lt;li&gt;모든 GC는 STW를 발생시키는데 Minor GC는 객체의 수명이 짧고 많은 객체를 검사하지 않기 때문에 매우 빨라 애플리케이션에 거의 영향을 주지 않습니다. &lt;b&gt;반면에 Major GC의 경우 살아있는 모든 객체를 검사해야 하기 때문에 오랜 시간이 걸립니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Serial GC&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCLTGI/btsCP5jef0h/4eSFz3ZsAuWII8IW1YRBi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCLTGI/btsCP5jef0h/4eSFz3ZsAuWII8IW1YRBi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCLTGI/btsCP5jef0h/4eSFz3ZsAuWII8IW1YRBi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCLTGI%2FbtsCP5jef0h%2F4eSFz3ZsAuWII8IW1YRBi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;462&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Serial GC는 하나의 쓰레드로 GC를 실행하는 방식입니다.&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나의 쓰레드로 GC를 실행시키다 보니 Stop The World 시간이 오래 걸립니다. 싱글 쓰레드 환경 및 Heap 영역이 매우 작을 때 사용하기 위한 방식 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parallel GC&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmIOdl/btsCN6iw7bZ/LueGR1b4PvfvUe3pay26D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmIOdl/btsCN6iw7bZ/LueGR1b4PvfvUe3pay26D1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmIOdl/btsCN6iw7bZ/LueGR1b4PvfvUe3pay26D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmIOdl%2FbtsCN6iw7bZ%2FLueGR1b4PvfvUe3pay26D1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;450&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Parallel GC는 여러 개의 쓰레드로 GC를 실행하는 방식입니다.&lt;/b&gt; 여러 개의 쓰레드를 사용하므로 Stop The World 시간이 짧아지고, 멀티 코어 환경에서 애플리케이션 처리 속도를 향상시키기 위해 사용됩니다. Java 8에서 기본으로 사용되는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CMS GC&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3yAZz/btsCRh4GQHq/aIN6kLPJGSpVK00Hs4kMMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3yAZz/btsCRh4GQHq/aIN6kLPJGSpVK00Hs4kMMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3yAZz/btsCRh4GQHq/aIN6kLPJGSpVK00Hs4kMMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3yAZz%2FbtsCRh4GQHq%2FaIN6kLPJGSpVK00Hs4kMMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;553&quot; height=&quot;476&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;CMS는 Concurrent-Mark-Sweep의 줄임말으로 Stop The World 시간을 최소화하기 위해 고안되었습니다. 대부분의 가비지 수집 작업을 애플리케이션 쓰레드와 동시에 수행해서 Stop The World 시간을 최소화 시키는 방식입니다. 하지만 메모리와 CPU를 많이 사용하고, Mark And Sweep 과정 이후 메모리 파편화를 해결하는 Compaction이 기본적으로 제공되지 않기 때문에 G1 GC가 등장하면서 대체되었다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;G1 GC&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GW9LI/btsCQrNjAVx/oP21Ukmk7lHkGBBKq5HKLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GW9LI/btsCQrNjAVx/oP21Ukmk7lHkGBBKq5HKLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GW9LI/btsCQrNjAVx/oP21Ukmk7lHkGBBKq5HKLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGW9LI%2FbtsCQrNjAVx%2FoP21Ukmk7lHkGBBKq5HKLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1062&quot; height=&quot;371&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;G1는 Garbage First의 줄임말으로 Heap을 일정 크기의 Region으로 나눠서 어떤 영역은 Young Generation, 어떤 영역은 Old Generation으로 활용합니다. 런타임에 G1 GC가 필요에 따라 영역별 Region 개수를 튜닝합니다. 이에 따라 Stop The World를 최소화 할수 있게 되어 &lt;b&gt;Java9 이상부터는 G1 GC를 기본 GC를 기본 실행방식&lt;/b&gt; 으로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작과정&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1D7G7/btsCTxMOx6t/GDzRaVMk3f8ZcPcdYAhcOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1D7G7/btsCTxMOx6t/GDzRaVMk3f8ZcPcdYAhcOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1D7G7/btsCTxMOx6t/GDzRaVMk3f8ZcPcdYAhcOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1D7G7%2FbtsCTxMOx6t%2FGDzRaVMk3f8ZcPcdYAhcOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1089&quot; height=&quot;265&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;265&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Young 영역에서는 기본 GC와 마찬가지로 용량이 일정 수준 이상으로 올라가면 Minor GC가 발생하면서 Survivor 영역으로 객체들이 복사되고 age 값이 증가합니다. age가 일정 수준으로 올라가면 old 영역으로 promotion 됩니다.&lt;/li&gt;
&lt;li&gt;G1에서 Full GC가 수행될 때는 다음과 같은 과정을 진행합니다.&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Initial Mark -&amp;gt; Root Region Scan -&amp;gt; Concurrent Mark -&amp;gt; Remark -&amp;gt; Cleanup -&amp;gt; Copy&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/blockquote&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;Initial Mark
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;STW가 발생합니다.&lt;/li&gt;
&lt;li&gt;Survivor 영역에서 Old 영역을 참조하고 있을 수 있는 영역들을 찾아 마킹합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Root Region Scan
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Initial Mark 단계에서 찾은 Survivor Region에 대한 GC 대상 객체 스캔 작업을 진행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Concurrent Mark
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 힙 영역에 대한 스캔으로 살아있는 객체가 존재하는 Region만 식별합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Remark
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;STW가 발생하며 최종적으로 살아남은 객체를 식별합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cleanup
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;STW가 발생하며 살아남은 객체가 가장 적은 Region의 GC대상을 제거 한 뒤 빈 영역을 Available Region으로 변경합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Copy
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GC 대상이었지만 Cleanup 단계에서 완전히 비워지지 않은 지역의 남은 객체를 Available Region으로 복사하여 조각모음(Compaction)을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가비지 컬렉터는 동적으로 할당한 메로리 영역 중 사용하지 않는 영역을 탐지하여 해제하는 역할&lt;/li&gt;
&lt;li&gt;자바 가비지 컬렉터는 Mark And Sweep 알고리즘을 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java8 : Parallel GC 사용&lt;/li&gt;
&lt;li&gt;Java9 이상 : G1 GC 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 과정&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;새로운 객체 생성은 Heap의 Eden 영역에 저장&lt;/li&gt;
&lt;li&gt;Eden 영역이 꽉차면 Minor GC가 수행되고, Reachable 객체는 Survival 0 영역으로 이동과 동시에 age-bit 1 상승&lt;/li&gt;
&lt;li&gt;2번 과정이 반복되면서 Survival 1 -&amp;gt; 0 -&amp;gt; 1 이동이 반복&lt;/li&gt;
&lt;li&gt;age-bit가 일정 값 이상이 되면 해당 객체에 대해 promotion 과정이 진행되어 Old Generation 영역으로 이동&lt;/li&gt;
&lt;li&gt;Old Generation 영역이 꽉차면 Major GC가 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=FMUpVA0Vvjw&amp;amp;list=PLo0ta52hn1uHQ5iQ3hAeRoMUeLJFIeRew&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; [10분 테코톡]   조엘의 GC&lt;/a&gt;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>CS</category>
      <category>Gc</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/54</guid>
      <comments>https://backtony.tistory.com/54#entry54comment</comments>
      <pubDate>Sat, 30 Dec 2023 21:27:02 +0900</pubDate>
    </item>
    <item>
      <title>HTTP/HTTPS</title>
      <link>https://backtony.tistory.com/53</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP와 HTTPS&lt;/h2&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;HTTP
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hypertext Transfer Protocol의 줄임말&lt;/li&gt;
&lt;li&gt;서로 다른 시스템들 사이에서 통신을 주고받게 하는 가장 기본적인 프로토콜입니다.&lt;/li&gt;
&lt;li&gt;서버에서 브라우저로 데이터를 전송하는 용도로 가장 많이 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTTPS
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hypertext Transfer Protocol Secure의 줄임말&lt;/li&gt;
&lt;li&gt;SSL(보안 소켓 계층)을 사용&lt;/li&gt;
&lt;li&gt;SSL은 서버와 브라우저 사이에 안전하게 암호화된 연결을 만들 수 있게 도와주고, 서버와 브라우저가 민감한 정보를 주고받을 때 해당 정보가 도난당하는 것을 막아줍니다.&lt;/li&gt;
&lt;li&gt;Header는 암호화하지 않고 HTTP Message Body만 암호화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSL/TLS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL/TLS는 HTTPS에서 암호화 시 사용하는 방식입니다. SSL의 업그레이드 버전이 TLS로, 보통 같은 의미로 사용합니다. SSL은 Secure Sokets Layer의 줄임말로, 웹 서버와 웹 브라우저간의 보안을 위해 만든 프로토콜입니다. 공개키/개인키, 대칭키 방식을 혼합해서 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대칭키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;247&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XRfIs/btsCOFrysuR/b1dImPbzs6z7P1cS663OC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XRfIs/btsCOFrysuR/b1dImPbzs6z7P1cS663OC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XRfIs/btsCOFrysuR/b1dImPbzs6z7P1cS663OC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXRfIs%2FbtsCOFrysuR%2Fb1dImPbzs6z7P1cS663OC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;921&quot; height=&quot;247&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;247&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대칭키 방식은 누구든지 암호화에 이용된 키를 가지고 있다면, 해당 데이터를 쉽게 복호화 할 수 있습니다. 즉, 클라이언트에서는 대칭키를 이용해 데이터를 암호화해서 서버로 보내면, 서버에서는 대칭키를 가지고 복호화해서 해서 데이터를 사용합니다. 만약 해커가 대칭키를 가지고 있고 중간에 데이터를 가로채간다면 해커도 해당 데이터를 읽을 수 있는 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공개키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l5uEB/btsCTBn2DPs/DfArPK28LrzBt9LI4kn7EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l5uEB/btsCTBn2DPs/DfArPK28LrzBt9LI4kn7EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l5uEB/btsCTBn2DPs/DfArPK28LrzBt9LI4kn7EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl5uEB%2FbtsCTBn2DPs%2FDfArPK28LrzBt9LI4kn7EK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;905&quot; height=&quot;308&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개키 방식은 서로 다른 키로 암호화 복호화를 수행하는 방식입니다. 서버측에서는 나에게 데이터를 보낼 때는 이 키를 사용해서 보내달라고 모두에게 공개키를 공유하고 해당 공개키로 암호화된 데이터를 복호화할 수 있는 개인키는 서버 혼자만 갖고 있습니다. 따라서, 중간에 해커가 데이터를 가로채더라도 복호화할 수 있는 개인키가 없기 때문에 해당 데이터를 읽을 수 없습니다. 하지만, 공개키 방식은 암호화 연산 시간이 더 소요되어 비용이 대칭키 방식보다 비용이 큽니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSL 통신 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL은 각 방식이 가진 단점 때문에 두 방식을 적절히 혼합하여 사용합니다.&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;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwYoVL/btsCMzFhjdn/fhfcd6DhrySaR5muDqd7r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwYoVL/btsCMzFhjdn/fhfcd6DhrySaR5muDqd7r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwYoVL/btsCMzFhjdn/fhfcd6DhrySaR5muDqd7r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwYoVL%2FbtsCMzFhjdn%2Ffhfcd6DhrySaR5muDqd7r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;225&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A에서 B로 접속 요청을 보냅니다.&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;891&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beqnTV/btsCSzD2n7N/Non1Q7cTblyZIBIktCVuVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beqnTV/btsCSzD2n7N/Non1Q7cTblyZIBIktCVuVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beqnTV/btsCSzD2n7N/Non1Q7cTblyZIBIktCVuVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeqnTV%2FbtsCSzD2n7N%2FNon1Q7cTblyZIBIktCVuVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;891&quot; height=&quot;279&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B는 A에게 자신의 공개키를 전송합니다.&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;968&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ91Xb/btsCOGjIMIb/WSTIHZAWY80Eqs4KIe40CK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ91Xb/btsCOGjIMIb/WSTIHZAWY80Eqs4KIe40CK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ91Xb/btsCOGjIMIb/WSTIHZAWY80Eqs4KIe40CK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ91Xb%2FbtsCOGjIMIb%2FWSTIHZAWY80Eqs4KIe40CK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;968&quot; height=&quot;361&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A는 B에게 받은 공개키로 자신의 대칭키를 암호화하여 다시 B에게 전송합니다.&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;957&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVl4WW/btsCN72PKiL/hbW0clbtLM7w6Fwwb1UPE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVl4WW/btsCN72PKiL/hbW0clbtLM7w6Fwwb1UPE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVl4WW/btsCN72PKiL/hbW0clbtLM7w6Fwwb1UPE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVl4WW%2FbtsCN72PKiL%2FhbW0clbtLM7w6Fwwb1UPE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;957&quot; height=&quot;423&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;423&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B는 자신의 공개키로 암호화된 A의 대칭키를 개인키로 복호화하여 A의 대칭키 값을 얻어냅니다.&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;892&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tEoRf/btsCMxAEQcP/PKpO9ssIcREsMhlCe0GTK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tEoRf/btsCMxAEQcP/PKpO9ssIcREsMhlCe0GTK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tEoRf/btsCMxAEQcP/PKpO9ssIcREsMhlCe0GTK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtEoRf%2FbtsCMxAEQcP%2FPKpO9ssIcREsMhlCe0GTK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;892&quot; height=&quot;279&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 얻어낸 대칭키를 사용하여 A와 B는 안전하게 통신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, 한쪽의 대칭키를 다른 쪽의 공개키로 암호화하여 전송하면 다른 쪽에서는 개인키로 복호화하여 반대편의 대칭키를 알아내고, 이 대칭키를 바탕으로 서로 통신하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유효한 사이트 확인&lt;/h2&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;565&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cn76p6/btsCN0inP6R/iHnFF9KCp2eZN7kVZqqsp1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cn76p6/btsCN0inP6R/iHnFF9KCp2eZN7kVZqqsp1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cn76p6/btsCN0inP6R/iHnFF9KCp2eZN7kVZqqsp1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcn76p6%2FbtsCN0inP6R%2FiHnFF9KCp2eZN7kVZqqsp1%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;565&quot; height=&quot;785&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;785&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;사이트는 사이트 인증서가 필요합니다. 사이트 인증서는 인증기관에서 발급하는 문서입니다. 이를 위해 사이트에서는 인증기관에게 사이트 정보와 사이트 공개키를 전송합니다.&lt;/li&gt;
&lt;li&gt;인증기관에서는 전달받은 데이터를 검증하고, 해당 데이터를 자신의 개인키로 암호화하여 인증서를 만듭니다. 이는 인증기관의 공개키로 복호화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;생성된 인증서를 사이트에게 전달합니다.&lt;/li&gt;
&lt;li&gt;인증기관은 자신의 공개키를 사용자에게 전달합니다. 사용자가 인증기관으로부터 전달받은 인증기관 공개키는 사용자 브라우저에 자동으로 내장됩니다. 여기까지는 사용자가 사이트에 접속하기 전의 내용입니다.&lt;/li&gt;
&lt;li&gt;사용자가 사이트에 접속을 요청합니다.&lt;/li&gt;
&lt;li&gt;사이트에서는 자신이 신뢰할 수 있는 사이트임을 증명하기 위해서 사이트 인증서를 전송합니다.&lt;/li&gt;
&lt;li&gt;사용자는 인증기관의 공개키를 브라우저 내에 내장하고 있으므로 공개키로 복호화하여 사이트 정보와 사이트의 공개키를 획득합니다.&lt;/li&gt;
&lt;li&gt;사이트의 공개키로 자신의 대칭키를 암호화합니다.&lt;/li&gt;
&lt;li&gt;암호화된 자신의 대칭키를 전송합니다.&lt;/li&gt;
&lt;li&gt;전달받은 사용자의 암호화된 대칭키를 자신의 개인키로 복호화하여 사용자의 대칭키를 획득합니다.&lt;/li&gt;
&lt;li&gt;사용자의 대칭키를 사용한 암호문으로 통신합니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>ETC</category>
      <category>CS</category>
      <category>HTTP</category>
      <category>https</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/53</guid>
      <comments>https://backtony.tistory.com/53#entry53comment</comments>
      <pubDate>Sat, 30 Dec 2023 21:12:35 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 인덱스</title>
      <link>https://backtony.tistory.com/52</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Index란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스란 (검색을 위해) &lt;b&gt;지정한 컬럼들을 기준으로 메모리 영역에 일종의 목차를 생성하는 것&lt;/b&gt; 입니다. 인덱스를 사용하게 되면 데이터 추가, 수정, 삭제의 성능을 희생하고 대신 &lt;b&gt;조회의 성능을 향상&lt;/b&gt; 시킵니다. 여기서 주의할 것은 update, delete 행위가 느린것이지 &lt;b&gt;update, delete를 하기 위해 해당 데이터를 조회하는 것은 인덱스가 있으면 빠르게 조회&lt;/b&gt; 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Clustered Index vs Non-Clustered Index&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 종류에는 clustered와 Non-Clustered 두 가지가 있습니다. MySQL의 InnoDB엔진에서는 인덱스가 B-Tree(균형 트리) 자료구조로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cluster Index(클러스터형 인덱스)&lt;/h3&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;897&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwSkwK/btsCTwAoOP8/YvahE6ZZ1pyBWN1r37LUM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwSkwK/btsCTwAoOP8/YvahE6ZZ1pyBWN1r37LUM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwSkwK/btsCTwAoOP8/YvahE6ZZ1pyBWN1r37LUM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwSkwK%2FbtsCTwAoOP8%2FYvahE6ZZ1pyBWN1r37LUM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;897&quot; height=&quot;475&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;한 테이블당 1개 (Primary Key)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;인덱스로 지정한 컬럼을 기준으로 &lt;b&gt;물리적으로 정렬&lt;/b&gt; 합니다.&lt;/li&gt;
&lt;li&gt;데이터 삽입 시 정렬이 이루어지기 때문에 입력, 수정, 삭제는 다소 느리지만 검색속도는 빠릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Non-Clustered Index(보조 인덱스)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TARZG/btsCU1fATPQ/mAQDhpK64TLb2VzL5JxX70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TARZG/btsCU1fATPQ/mAQDhpK64TLb2VzL5JxX70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TARZG/btsCU1fATPQ/mAQDhpK64TLb2VzL5JxX70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTARZG%2FbtsCU1fATPQ%2FmAQDhpK64TLb2VzL5JxX70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;795&quot; height=&quot;796&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;한 테이블당 여러 개 가능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 당 3~4개를 권장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카디널리티가 높은 순&lt;/b&gt; 으로 잡습니다.(중복도가 낮으면 카디널리티가 높습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 자체는 정렬되지 않고, 인덱스 값을 기준으로 정렬하여 내부적으로 인덱스 페이지가 생성됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;검색 속도는 Cluster에 비해 느리지만 입력, 수정, 삭제가 빠릅니다.&lt;/li&gt;
&lt;li&gt;Cluster 인덱스처럼 리프 페이지가 바로 데이터를 나타내지 않고, 데이터 페이지의 주소값을 갖고 있어 한단계 더 나아가야 하므로 검색의 경우 Cluster보다 느리지만 데이터 자체의 정렬작업이 없으므로 입력, 수정, 삭제는 Cluster에 비해 빠릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cluster + Non-Clustered Index&lt;/h2&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;932&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vOFLT/btsCQrNjopf/3up5WMqDWBxIHMFDXvlhmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vOFLT/btsCQrNjopf/3up5WMqDWBxIHMFDXvlhmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vOFLT/btsCQrNjopf/3up5WMqDWBxIHMFDXvlhmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvOFLT%2FbtsCQrNjopf%2F3up5WMqDWBxIHMFDXvlhmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;878&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 테이블에는 클러스터형 인덱스와 보조 인덱스가 같이 있는 경우가 많습니다. 이때는 B트리의 내부적으로 가리키는 값들이 변화합니다. 회원 테이블에서 Name에 보조 인덱스, UserId를 Primary Key로 Cluster 인덱스를 세팅했다고 가정하고 위 그림을 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;select * from Member WHERE name = '홍길동';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;name에 보조 인덱스가 걸려있으므로 보조 인덱스에 대한 루트 -&amp;gt; 리프 페이지에서 값을 찾습니다. 이때 리프 페이지의 값을 보면 기존의 보조 인덱스 페이지에서는 데이터 페이지의 주소값이 들어있었는데 이제는 &lt;b&gt;Cluster 인덱스 값&lt;/b&gt; 이 들어있습니다. 만약 여기서 select 절에 원하는 필드가 name 값이거나 Cluster 인덱스 값이라면 끝이 나겠지만, member의 다른 컬럼까지 원하게 되면 해당 Cluster 인덱스를 갖고 Cluster의 인덱스 루트 페이지에서부터 검색을 시작합니다. 이후는 기존 Cluster 인덱스의 동작 과정과 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티 인덱스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 인덱스란 &lt;b&gt;두개 이상의 필드를 조합하여 생성한 인덱스&lt;/b&gt; 를 의미합니다. 그냥 각각에 인덱스를 주고 사용해서 조회하면 되지 않을까 생각할 수 있습니다. 하지만 MySQL은 단일 쿼리를 실행할 때 &lt;b&gt;하나의 테이블 당 하나의 인덱스&lt;/b&gt; 만 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create table member(
    id bigint not null auto_increment,
    name varchar(255) NOT null,
    age int not null,
    address varchar(255) not null,
    primary key (id),
    key idx_name(name),
    key idx_address(address)
);

select * from member where name='홍길동' and address='경기도';&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;위와 같이 각각의 인덱스를 where 문에 넣어주고 쿼리를 날리면 MySQL은 &lt;b&gt;인덱싱된 데이터 중 행이 적은 것의 인덱스를 사용해서 동작&lt;/b&gt; 합니다. 즉, 한개의 인덱스만 사용하는 것입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create table member1(
    id bigint not null auto_increment,
    name varchar(255) NOT null,
    age int not null,
    address varchar(255) not null,
    primary key (id),
    key idx_multi(name,address)    
);

-- 문제 없음
select * from member1 where name ='홍길동' and address='강남';
select * from member1 where address='강남' and name ='홍길동';
select * from member1 where name ='홍길동';

-- 문제 발생
select * from member1 where address='강남';&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;위와 같이 멀티 인덱스를 생성하고 조회 시 주의해야할 점이 있습니다. 기본적으로 멀티 인덱스를 모두 조건문에 넣거나, 멀티 인덱스의 가장 첫 인덱스를 조건문에 넣으면 조회 시 인덱스를 사용해서 조회하게 됩니다. 하지만 address 만 사용해서 조회 시 인덱스를 사용하지 않고 Full Table Scan 을 해버립니다. 이유는 멀티 인덱스는 첫 인덱스인 name으로 우선 정렬되고 그 다음으로 address가 name에 의존하여 정렬되기 때문입니다. &lt;b&gt;즉, 멀티 인덱스는 인덱스 순서대로 정렬되어 바로 앞의 인덱스에 의존해서 정렬됩니다.&lt;/b&gt; &lt;b&gt;따라서 멀티 인덱스의 경우, 인덱스를 사용하고 싶다면 조건문에 모든 인덱스를 포함하거나, 인덱스의 순서를 지켜야 합니다.&lt;/b&gt; 단일 인덱스를 여러개 사용하는 것보다 같이 조건에 사용하는 인덱스가 있다면 묶어서 멀티 인덱스로 사용하는 것이 조회측면에서는 좋으나, 멀티 인덱스는 단일 컬럼 인덱스보다 비효율적으로 Index, Update, Delete를 수행하기 때문에 사용에 신중해야 합니다. 가급적이면 업데이트가 안되는 값을 선정하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 컬럼에 인덱스를 설정할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;카디널리티가 높은 순으로&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복도가 낮은것 우선&lt;/li&gt;
&lt;li&gt;멀티 인덱스를 만들 때도 카디널리티가 높은 순으로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용도가 높은 것&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where, join에서 절에 자주 활용되는지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;업데이트가 빈번하지 않은 컬럼&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스의 개수는 3~4개&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 많은 인덱스는 새로운 Row를 등록할 때마다 인덱스를 추가해야하고, 수정/삭제시마다 인덱스 수정이 필요하므로 이슈가 있습니다.&lt;/li&gt;
&lt;li&gt;인덱스 역시 공간을 차지하기에 많아질수록 이슈가 있습니다.&lt;/li&gt;
&lt;li&gt;인덱스가 많아질 수록 옵티마이저가 잘못된 인덱스를 선택할 확률이 높습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스 주의사항&lt;/h2&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;in은 =이 여러번 수행되는 연산이므로 인덱스가 적용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;in에는 서브쿼리보다는 join으로 해결한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;or은 비교할 row가 늘어나기 때문에 Full scan이 발생할 확률이 높아 주의한다.&lt;/li&gt;
&lt;li&gt;인덱스로 사용된 컬럼값은 연산하지 않고 그대로 사용해야 적용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where salary * 1000 = 10000; 이렇게 왼쪽에 연산을 하면 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스의 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;범위 조건 이후의 인덱스는 인덱스를 타지 않는다.&lt;/li&gt;
&lt;li&gt;인덱스의 조건문 사용 순서와 select절 사용 순서를 일치시킬 필요는 없다.&lt;/li&gt;
&lt;li&gt;group by
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스의 순서를 지켜야한다.&lt;/li&gt;
&lt;li&gt;인덱스 순서가 지켜진다면 뒤에 인덱스 컬럼을 명시하지 않아도 적용된다.&lt;/li&gt;
&lt;li&gt;인덱스 이외의 것이 포함되면 안된다.&lt;/li&gt;
&lt;li&gt;where의 동등 조건과 사용시 적용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(a,b,c) 일때 b가 where문에 나와되지만 group by는 a,c 로 순서를 일치시켜야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;order by
&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;where의 동등 조건과 사용시 적용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(a,b,c) 인덱스 -&amp;gt; where a=1 order by b,c&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;where + group by + order by
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;group by와 order by 모두 각각의 인덱스 적용 조건읆 만족해야만 적용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;8.0 버전 이전에서는 인덱스 생성시 desc를 실질적으로 지원하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;asc로 만들어진 인덱스를 앞에서부터 읽느냐 뒤에서부터 읽느냐의 차이일 뿐이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아래 내용은 위와 동일한 내용이지만 예시를 들어가면서 조금 길게 작성한 내용입니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;멀티 인덱스에서 between, like, &amp;lt;, &amp;gt; 등 범위조건은 해당 컬럼은 인덱스를 타지만, 그 뒤 인덱스 컬럼들은 인덱스가 사용되지 않습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(a,b,c)으로 멀티 인덱스가 잡혀있는데 조회 쿼리를 where a=XX and c=YY and b &amp;gt; ZZ 으로 잡으면 c는 인덱스가 사용되지 않습니다.&lt;/li&gt;
&lt;li&gt;즉, b에서 범위 조건이 걸렸으므로 a, b까지는 인덱스를 사용하고 c는 인덱스를 타지 않는다는 의미 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;=, in은 다음 컬럼도 인덱스를 사용합니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;in은 결국 = 를 여러번 실행시킨 것이기 때문입니다.&lt;/li&gt;
&lt;li&gt;단, &lt;b&gt;in은 인자값으로 서브쿼리를 넣으면 성능상 이슈가 발생합니다. -&amp;gt; join을 하는 것이 성능상 좋습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;in의 인자로 서브쿼리가 들어가면 서브쿼리의 외부가 먼저 실행되고 in 서브쿼리는 체크 조건으로 반복해서 실행되서 성능상 이슈가 있습니다.&lt;/li&gt;
&lt;li&gt;MySQL 5.6 부터는 서브쿼리를 사용하면 내부적으로 join으로 풀어서 실행하지만, 동작과정을 보면 서브 쿼리가 먼저 실행되어 그 결과로 임시 테이블을 생성한 뒤 그것을 메인 테이블과 Join해서 결과를 반환하게 됩니다.&lt;/li&gt;
&lt;li&gt;임시 테이블을 만들기 때문에 일반 Join에 비해 성능이 조금 떨어지기 때문에 가능하면 Join을 사용하는게 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AND 연산자는 각 조건들이 읽어와야할 ROW수를 줄이는 역할을 하지만, or 연산자는 비교해야할 ROW가 더 늘어나기 때문에 풀 테이블 스캔이 발생할 확률이 높습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;where에서 or를 사용할 때는 주의가 필요합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스로 사용된 &lt;b&gt;컬럼값 그대로 사용해야만 인덱스가 사용&lt;/b&gt; 됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where salary * 10 &amp;gt; 10000; 는 인덱스를 사용하지 못하지만, where salary &amp;gt; 15000/10;은 인덱스를 사용합니다.&lt;/li&gt;
&lt;li&gt;즉, where 조건의 왼쪽에 나오는 인덱스 변수는 추가적인 작업을 하면 안됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스 where 문에서 꼭 인덱스 순서와 select 절의 조회 순서를 지킬 필요는 없습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스 컬럼들이 조회조건에 포함되어 있는지&lt;/b&gt; 가 중요합니다.&lt;/li&gt;
&lt;li&gt;단, 옵티마이저가 조회 조건의 컬럼을 인덱스 컬럼 순서에 맞춰 재배열하는 과정이 추가되긴 하지만 거의 차이가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스의 경우, Group By 절에 명시된 컬럼은 인덱스 컬럼 순서와 같아야 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index가 (a,b,c) 라면 아래 모두 적용이 안됩니다.&lt;/li&gt;
&lt;li&gt;group by b&lt;/li&gt;
&lt;li&gt;group by b, a&lt;/li&gt;
&lt;li&gt;group by a, c, b&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스의 경우, 앞에 있는 컬럼이 group by에 명시되지 않으면 인덱스가 적용되지 않습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index가 (a,b,c) 라면 아래 모두 적용이 안됩니다.&lt;/li&gt;
&lt;li&gt;group by b, c&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스의 경우, 인덱스에 없는 컬럼이 group by에 포함되어 있으면 인덱스가 적용되지 않습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index(a,b,c) -&amp;gt; group by a,b,c,d -&amp;gt; 적용 X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스의 경우, 뒤에 있는 컬럼이 Group By에 명시되지 않아도 인덱스가 적용됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index가 (a,b,c) 라면 아래 모두 적용 됩니다.&lt;/li&gt;
&lt;li&gt;group by a&lt;/li&gt;
&lt;li&gt;group by a, b&lt;/li&gt;
&lt;li&gt;group by a, b, c&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Where 조건과 Group by가 함께 사용되면 &lt;b&gt;Where 조건이 동등 비교일 경우&lt;/b&gt; group by 절에 해당 컬럼이 없어도 인덱스가 적용됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 (a,b,c) 일때, 다음은 모두 인덱스가 적용됩니다.&lt;/li&gt;
&lt;li&gt;where a=1 group by b,c&lt;/li&gt;
&lt;li&gt;where a=1 and b=2 group by c&lt;/li&gt;
&lt;li&gt;where b=1 group by a,c&lt;/li&gt;
&lt;li&gt;만약 아래와 같이 group by에서 순서를 바꾸게 된다면 임시 테이블을 만들게 되므로 성능이 떨어집니다.&lt;/li&gt;
&lt;li&gt;where a=1 group by c,b&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;where 조건문에 동등 조건이 아닌 like와 같은 연산을 하게 되면 using temporary(임시테이블)이 별도로 생성되고 그 안에서 using filesort(정렬)이 발생합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 (a,b,c) 일때, 아래는 의도한 대로 인덱스를 타지 못합니다.&lt;/li&gt;
&lt;li&gt;where a like 'TAKE%' group by b,c&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;8.0 버전 이전에서는 인덱스 생성시 desc를 실질적으로 지원하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL에서는 인덱스 생성시 컬럼 마다 asc/desc를 정할 수 있는 것처럼 보이나 8.0 이전 버전까지는 문법만 지원되고 실질적으로 Desc 인덱스가 지원되는 것은 아닙니다. 단지 Asc로 만들어진 인덱스를 앞에서부터 읽을 것인지, 뒤에서부터 읽을 것인지에 차이만 있을 뿐입니다. 즉, 인덱스 컬럼 중 특정 컬럼만 Desc가 되지 않으며 인덱스 컬럼 전체를 asc 혹은 desc 스캔하는 방법 뿐입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스 (a,b,c)의 order by에서 인덱스가 적용이 안되는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;order by의 경우, 순서도 지키고, 모두 포함하고, 다른 컬럼이 추가되면 안됩니다.&lt;/li&gt;
&lt;li&gt;order by b,c&lt;/li&gt;
&lt;li&gt;order by a,c&lt;/li&gt;
&lt;li&gt;order by a,b,c&lt;/li&gt;
&lt;li&gt;order by a, b desc, c&lt;/li&gt;
&lt;li&gt;order by a,b,c,d&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;멀티 인덱스 (a,b,c)에서 where문과 order by를 함께 사용시 where문이 동등비교라면 인덱스가 가능합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where a=1 order by b,c&lt;/li&gt;
&lt;li&gt;where a = 1, b = 1 order by c&lt;/li&gt;
&lt;li&gt;동등비교가 아니라면 group by와 마찬가지로 인덱스가 되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;where + group by + order by 멀티 인덱스(a,b,c)인 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;group by와 order by가 함께 사용된 경우라면, 둘다 모두 인덱스를 타야지만 인덱스가 적용됩니다.&lt;/li&gt;
&lt;li&gt;group by a,b,c order by a,b,c -&amp;gt; 적용&lt;/li&gt;
&lt;li&gt;group by a,b,c order by b,c -&amp;gt; 미적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커버링 인덱스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스란 &lt;b&gt;모든 항목이 인덱스 컬럼으로 이루어진 상태&lt;/b&gt; 를 의미합니다. 앞서 클러스터 인덱스와 논클러스터 인덱스를 함께 사용하게 될 경우, 논클러스터 인덱스에서 1차적으로 조회하고 cluster 인덱스를 사용해서 2차적으로 조회하게 된다고 설명했습니다. 커버링 인덱스를 사용할 경우, 추가적인 정보가 필요 없이 인덱스로만 결과값을 도출해낼 수 있기 때문에 2차적인 접근이 필요 없습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create table member3(
    id bigint not null auto_increment,
    name varchar(255) not null,
    age int not null,
    primary key(id),
    key idx_name(name)
);

explain select * from member3 where id=1;
explain select m.id from member2 m where name='홍길동';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HpA2k/btsCUAPYxYE/f7isbJ3QIjF2pSXezEuYZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HpA2k/btsCUAPYxYE/f7isbJ3QIjF2pSXezEuYZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HpA2k/btsCUAPYxYE/f7isbJ3QIjF2pSXezEuYZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHpA2k%2FbtsCUAPYxYE%2Ff7isbJ3QIjF2pSXezEuYZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;119&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;explain을 이용해서 위 쿼리를 살펴보면 첫 번째 쿼리의 경우 key 항목에는 사용된 인덱스가, Extra가 빈값으로 나옵니다.&lt;br /&gt;즉, where 절에는 인덱스가 사용되었지만 select 절의 필드를 완성하는데는 데이터 블록 접근(2차 접근)이 있었다는 의미입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opGKc/btsCMnZe6GU/mM0AkRAYXKZLpKkYtkjhBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opGKc/btsCMnZe6GU/mM0AkRAYXKZLpKkYtkjhBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opGKc/btsCMnZe6GU/mM0AkRAYXKZLpKkYtkjhBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FopGKc%2FbtsCMnZe6GU%2FmM0AkRAYXKZLpKkYtkjhBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;957&quot; height=&quot;91&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;91&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반면에 두 번째 쿼리를 보면 key 항목에는 인덱스가, Extra 항목에는 Using Index라고 표기됩니다. Using Index는 쿼리 전체가 인덱스 컬럼값으로 다 채워진 즉, 커버링 인덱스가 사용된 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://jojoldu.tistory.com/243&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; [mysql] 인덱스 정리 및 팁&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://jojoldu.tistory.com/476&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 1. 커버링 인덱스 (기본 지식 / WHERE / GROUP BY)&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://jojoldu.tistory.com/481?category=761883&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 2. 커버링 인덱스 (WHERE + ORDER BY / GROUP BY + ORDER BY )&lt;/a&gt;&lt;/p&gt;</description>
      <category>Database</category>
      <category>CS</category>
      <category>index</category>
      <category>mysql</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/52</guid>
      <comments>https://backtony.tistory.com/52#entry52comment</comments>
      <pubDate>Sat, 30 Dec 2023 21:01:58 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 아키텍처</title>
      <link>https://backtony.tistory.com/51</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL 아키턱처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zQKrR/btsCRg5MA6Q/kqd8RieaL2ro0mNW1RHCW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zQKrR/btsCRg5MA6Q/kqd8RieaL2ro0mNW1RHCW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zQKrR/btsCRg5MA6Q/kqd8RieaL2ro0mNW1RHCW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzQKrR%2FbtsCRg5MA6Q%2Fkqd8RieaL2ro0mNW1RHCW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1151&quot; height=&quot;615&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 접속 클라이언트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 프로그래밍 언어에 대한 접속 API 제공&lt;/li&gt;
&lt;li&gt;쉘 스크립트를 통해 접속 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MySQL 엔진
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 접속과 SQL 요청 처리 담당&lt;/li&gt;
&lt;li&gt;쿼리 파서, 전처리기, 옵티마이저(요청된 SQL문을 최적화해서 실행하기 위한 실행계획을 짜는 역할), 실행 엔진 등으로 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MySQL 스토리지 엔진
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 실제로 디스크에 저장하거나 읽어오는 역할&lt;/li&gt;
&lt;li&gt;옵티마이저가 작성한 실행 계획에 따라서 적절히 실행&lt;/li&gt;
&lt;li&gt;MySQL 엔진이 스토리 엔진을 호출할 때 사용하는 API를 핸들러 API라고 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;운영체제, 하드웨어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 테이블 데이터와 로그 데이터를 파일로 저장하는 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿼리 실행 과정&lt;/h2&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;671&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH7LQO/btsCOEsG2yO/GsdnENg8KJctniyScogHr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH7LQO/btsCOEsG2yO/GsdnENg8KJctniyScogHr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH7LQO/btsCOEsG2yO/GsdnENg8KJctniyScogHr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH7LQO%2FbtsCOEsG2yO%2FGsdnENg8KJctniyScogHr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;691&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;691&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;사용자가 SQL 요청을 MySQL로 보냅니다.&lt;/li&gt;
&lt;li&gt;가장 먼저 쿼리 캐시를 만나는데 쿼리 캐시는 쿼리 요청 결과를 캐싱하는 모듈로 이를 통해 동일한 SQL 요청에 대한 결과를 빠르게 받을 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 캐시는 캐싱하고 있는 데이터의 테이블이 변경된다면 이 데이터를 삭제하는 과정에서 쿼리 캐시에 접근하는 쓰레드에 Lock이 걸리면서 심각한 동시 처리 성능을 저하하기 때문에 MySQL 8.0 버전부터는 삭제되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쿼리 파서는 기본적인 SQL 문장 오류를 체크하고 의미 있는 단위의 토큰으로 쪼갠 다음 트리(Parse Tree)로 만듭니다. 이를 통해 쿼리를 실행합니다.&lt;/li&gt;
&lt;li&gt;전처리기는 Parse Tree를 기반으로 SQL의 문장 구조를 검사하고, 파스 트리의 토큰을 하나씩 검사하면서 토큰에 해당하는 테이블 이름이나 컬럼 등이 실제로 존재하는지 체크하고 접근 권한에 대해서도 체크합니다.&lt;/li&gt;
&lt;li&gt;옵티마이저는 SQL 실행을 최적화해서 실행 계획을 만듭니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;규칙 기반 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;옵티마이저에 내장된 우선순위에 따라 실행 계획 수립&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비용 기반 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL을 처리하는 다양한 방법을 만들어 두고, 각 방법의 비용과 테이블 통계 정보를 바탕으로 실행 계획 수립&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쿼리 실행 엔진은 옵티마이저가 만든 실행 계획대로 스토리지 엔진을 호출해서 쿼리를 수행합니다.&lt;/li&gt;
&lt;li&gt;스토리지 엔진은 쿼리 실행 엔진이 요청하는 대로 데이터를 디스크로 저장하고 읽습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대표적으로 InnoDB가 있고, 플러그인 형태로 제공되어 사용자가 원하는 엔진 선택이 가능합니다.&lt;/li&gt;
&lt;li&gt;핸들러 API에 의해 동작하고 핸들러라고도 불립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;InnoDB 스토리지 엔진&lt;/h2&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;Primary Key에 의한 클러스터링&lt;/li&gt;
&lt;li&gt;트랜잭션 지원
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MVCC, Redo &amp;amp; undo 로그, 레코드 단위 잠금&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;InnoDB 버터풀 &amp;amp; 어댑티브 해시 인덱스 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징 1 - PK에 의한 클러스터링&lt;/h3&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;978&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4WIaa/btsCTvam0ol/mhBqdOvavQatZzgXhROyt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4WIaa/btsCTvam0ol/mhBqdOvavQatZzgXhROyt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4WIaa/btsCTvam0ol/mhBqdOvavQatZzgXhROyt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4WIaa%2FbtsCTvam0ol%2FmhBqdOvavQatZzgXhROyt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;978&quot; height=&quot;473&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;473&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레코드를 PK순으로 정렬해서 저장&lt;/li&gt;
&lt;li&gt;PK 인덱스 자동 생성&lt;/li&gt;
&lt;li&gt;PK를 통해서만 레코드에 접근 가능&lt;/li&gt;
&lt;li&gt;PK를 통한 범위 검색이 매우 빠름&lt;/li&gt;
&lt;li&gt;클러스터링 때문에 쓰기 성능 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징 2 - 트랜잭션 MVCC(Multi Version Concurrency Control)&lt;/h3&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;801&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k7s6I/btsCSA3YLpu/ArddpEBn5KmCpR8I5PIhl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k7s6I/btsCSA3YLpu/ArddpEBn5KmCpR8I5PIhl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k7s6I/btsCSA3YLpu/ArddpEBn5KmCpR8I5PIhl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk7s6I%2FbtsCSA3YLpu%2FArddpEBn5KmCpR8I5PIhl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2036&quot; height=&quot;801&quot; data-origin-width=&quot;2036&quot; data-origin-height=&quot;801&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;트랜잭션 격리 레벨에 따라 조회되는 데이터가 달라지게 하는 기술을 MVCC&lt;/b&gt; 라고 합니다. &lt;b&gt;MVCC를 통해 레코드에 잠금을 걸지 않고도 트랜잭션 격리레벨에 따라 일관된 읽기를 할 수 있습니다.&lt;/b&gt; InnoDB 버퍼풀은 변경된 데이터를 디스크에 반영하기 전까지 잠시 버퍼링 하는 공간입니다. 언두 로그는 변경되기 이전 데이터를 백업해 두는 공간입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터를 Insert하고 커밋하면 InnoDB 버퍼풀에 새로 삽입한 레코드가 생깁니다.&lt;/li&gt;
&lt;li&gt;Update 쿼리를 날리면 InnoDB 레코드 값이 변경되고 이전 레코드 값은 언두 로그로 복사됩니다.&lt;/li&gt;
&lt;li&gt;이때 다른 트랜잭션이 해당 레코드를 조회한다면 데이터베이스에 설정된 트랜잭션 격리 수준에 따라 다릅니다.&lt;/li&gt;
&lt;li&gt;READ_UNCOMMITTED 라면 InnoDB 버퍼풀 값을, READ_COMMITTED, REATABLE_READ, SERIALAZBLE 이라면 언두 로그에 있는 값을 읽습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cf) 격리 수준&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;Dirty read
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경 사항이 반영되지 않은 값을 다른 트랜잭션에서 읽는 현상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Nonrepeatable read
&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;즉, select 조회 후 다른 트랜잭션에서 해당 값을 update 해버리고, 현재 트랜잭션에서 같은 select 조회했을 때 데이터가 다른 값을 갖는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Phantom read
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 트랜잭션 안에서 &lt;b&gt;일정범위의&lt;/b&gt; 레코드를 두 번 이상 읽을 때, 첫 번째 쿼리에서 없던 유령 레코드가 두 번째 쿼리에서 나타나는 현상&lt;/li&gt;
&lt;li&gt;첫 조회 당시 A, B, C 데이터가 있었는데 같은 트랜잭션 내에서 같은 조회 쿼리 날릴 경우 A, B, C, D 데이터가 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 세 문제에 대한 대응 책으로 4개의 트랜잭션 고립 단계가 존재합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;READ UNCOMMITTED
&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;Dirty read 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;READ COMMITTED
&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;Dirty read 방지&lt;/li&gt;
&lt;li&gt;Nonrepeatable read 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;REPEATABLE READ
&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;Dirty read, Nonrepeatable read 방지&lt;/li&gt;
&lt;li&gt;Phantom read 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SERIALIZABLE
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 엄격한 수준으로 앞선 문제들을 방지합니다.&lt;/li&gt;
&lt;li&gt;동시성 처리 성능이 급격히 하락합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징 3 - Undo Log &amp;amp; Redo Log&lt;/h3&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;Undo Log
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경되기 이전 데이터를 백업합니다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 보장되기 때문에 롤백 시 언두 로그에 백업된 데이터로 복원해 주면 됩니다.&lt;/li&gt;
&lt;li&gt;트랜잭션 격리 수준도 보장해 줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Redo Log
&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;하드웨어 또는 소프트웨어 문제로 MySQL이 비정상 종료되면 Redo Log를 통해 복원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징 4 - 레코드 단위 잠금&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서 데이터를 변경할 때는 동시성 문제를 고려하여 레코드에 대한 접근을 막는데 이를 &lt;b&gt;잠금이라고&lt;/b&gt; 합니다.&lt;br /&gt;InnoDB는 &lt;b&gt;레코드 단위로 잠금을&lt;/b&gt; 걸기 때문에 동시처리 성능이 좋습니다. 실질적으로는 레코드 자체를 잠그는 것이 아니라 &lt;b&gt;인덱스 레코드를&lt;/b&gt; 잠그게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 상황을 예시로 진행하겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;User 테이블에 5000 개의 레코드&lt;/li&gt;
&lt;li&gt;성씨 칼럼은 인덱스가 적용&lt;/li&gt;
&lt;li&gt;성씨 칼럼은 '박'인 레코드는 300개 존재&lt;/li&gt;
&lt;li&gt;성씨는 '박'이고 이름은 '병욱'인 레코드는 1개만 존재&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;1466&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1hYGx/btsCTXEuVpn/h9VOWRP8X02Hs1maK86duK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1hYGx/btsCTXEuVpn/h9VOWRP8X02Hs1maK86duK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1hYGx/btsCTXEuVpn/h9VOWRP8X02Hs1maK86duK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1hYGx%2FbtsCTXEuVpn%2Fh9VOWRP8X02Hs1maK86duK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1466&quot; height=&quot;464&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;업데이트할 레코드를 검색할 때 사용된 박 씨 인덱스 레코드가 모두 잠기게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RfYPT/btsCP5wLvA7/rvJhoPp2mc8KRZb06aEB40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RfYPT/btsCP5wLvA7/rvJhoPp2mc8KRZb06aEB40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RfYPT/btsCP5wLvA7/rvJhoPp2mc8KRZb06aEB40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRfYPT%2FbtsCP5wLvA7%2FrvJhoPp2mc8KRZb06aEB40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;541&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 성씨 인덱스가 없었다면 기본으로 생성된 PK 인덱스를 사용하여 테이블을 풀스캔하게 되면서 5000 개의 데이터에 대해 잠금이 걸리게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDLcEN/btsCTydTnLX/tdsB2MKnrrZrvnjHoVRxMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDLcEN/btsCTydTnLX/tdsB2MKnrrZrvnjHoVRxMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDLcEN/btsCTydTnLX/tdsB2MKnrrZrvnjHoVRxMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDLcEN%2FbtsCTydTnLX%2FtdsB2MKnrrZrvnjHoVRxMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;505&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 성씨와 이름에 대한 복합 인덱스를 생성했다면 딱 한 개의 레코드에 대해서만 잠금이 걸리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징 5 - 버퍼풀&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼풀의 용도는 데이터 캐싱과 쓰기 지연 버퍼 2가지로 사용됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 캐싱&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼풀은 SQL 요청 결과를 일정한 크기의 페이지 단위로 캐싱합니다. 운영체제가 가상 메모리를 효율적으로 사용하기 위해 페이징 하는 것처럼 데이터베이스도 테이블 데이터에 대해 페이징 합니다. InnoDB는 페이지 교체 알고리즘으로 LRU 알고리즘을 사용하고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;쓰기 지연 버퍼&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Insert, Update, Delete 명령으로 변경된 페이지를 더티 페이지라고 합니다. InnoDB는 더티 페이지들을 모았다가 주기적으로 이벤트를 발생시켜 한 번에 디스크에 반영합니다.(JPA 영속성 콘텍스트와 유사) 이렇게 변경된 데이터를 한 번에 모았다가 처리하는 이유는 랜덤 I/O를 줄이기 위해서입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어댑티브 해시 인덱스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어댑티브 해시 인덱스는 페이지에 빠르게 접근하기 위한 키와 페이지 주소값 쌍으로 이루어진 해시 자료구조 기반 인덱스입니다. 어댑티브 해시 인덱스는 사용자가 자주 요청하는 데이터에 대해서 InnoDB가 자동으로 만들어줍니다. 이를 통해 원하는 페이지에 빠르게 접근할 수 있기 때문에 쿼리를 더 빠르게 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=vQFGBZemJLQ&amp;amp;list=PLo0ta52hn1uHQ5iQ3hAeRoMUeLJFIeRew&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; [10분 테코톡]   우기의 MySQL 아키텍처&lt;/a&gt;&lt;/p&gt;</description>
      <category>Database</category>
      <category>CS</category>
      <category>mysql</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/51</guid>
      <comments>https://backtony.tistory.com/51#entry51comment</comments>
      <pubDate>Sat, 30 Dec 2023 20:56:13 +0900</pubDate>
    </item>
    <item>
      <title>Blocking과 Non-Blocking, Sync와 Async</title>
      <link>https://backtony.tistory.com/50</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Blocking vs Non-Blocking&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;두 가지의 차이점은 &lt;b&gt;다른 주체가 작업할 때 자신이 코드를 실행할 제어권이 있는지 없는지로 판단할&lt;/b&gt; 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Blocking&lt;/h3&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;665&quot; data-origin-height=&quot;697&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmmDgM/btsCOGDYzGn/1fqu3Wcbz4kHPfcuLfLIN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmmDgM/btsCOGDYzGn/1fqu3Wcbz4kHPfcuLfLIN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmmDgM/btsCOGDYzGn/1fqu3Wcbz4kHPfcuLfLIN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmmDgM%2FbtsCOGDYzGn%2F1fqu3Wcbz4kHPfcuLfLIN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;697&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;697&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Blocking 자신의 작업을 진행하다가 다른 주체의 작업이 시작되면 &lt;b&gt;제어권을 다른 주체로 넘깁니다.&lt;/b&gt; 따라서 자신은 제어권이 없기 때문에 실행할 수 없고, 다른 주체가 실행을 완료하고 제어권을 돌려줄 때까지 아무 작업도 할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Non-Blocking&lt;/h3&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;597&quot; data-origin-height=&quot;685&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dx5ais/btsCN4SyN7G/2CoYUiOo38rzFdatO8ZxU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dx5ais/btsCN4SyN7G/2CoYUiOo38rzFdatO8ZxU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dx5ais/btsCN4SyN7G/2CoYUiOo38rzFdatO8ZxU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdx5ais%2FbtsCN4SyN7G%2F2CoYUiOo38rzFdatO8ZxU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;597&quot; height=&quot;685&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;685&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Non-Blocking은 다른 주체의 작업에 &lt;b&gt;관련 없이 자신이 제어권을 갖고 있습니다.&lt;/b&gt; 따라서, 자신은 계속 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sync vs Async&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지의 차이점은 &lt;b&gt;결과를 돌려주었을 때 순서와 결과에 관심이 있는지 없는지&lt;/b&gt; 에 차이가 있습니다. 아래 설명부터 A는 Application이고 B는 Kernel로 표현하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sync(동기)&lt;/h3&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;837&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GnSDw/btsCOzLyioS/lhaKwPCogiLZbKV3OK6PV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GnSDw/btsCOzLyioS/lhaKwPCogiLZbKV3OK6PV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GnSDw/btsCOzLyioS/lhaKwPCogiLZbKV3OK6PV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGnSDw%2FbtsCOzLyioS%2FlhaKwPCogiLZbKV3OK6PV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;837&quot; height=&quot;579&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;동기는 함수 A가 B를 호출한 뒤, A는 다른 일을 하거나 멈춰있고 B가 결괏값을 반환해 주면 해당 결괏값을 가지고 작업을 처리하는 방식입니다. A가 다른 일을 하거나 멈춰있다고 표현했는데 이는 blocking이냐 non-blocking이냐에 따라 다릅니다. blocking의 경우에는 제어권이 없기 때문에 멈춰있고, non-blocking의 경우에는 다른 작업을 하면서 중간중간에 B에게 작업이 끝났는지 물어보고 끝났으면 결괏값을 가져와서 작업을 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Async(비동기)&lt;/h3&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;837&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rw9D6/btsCTBn2lQ0/S0kHqRy5k9kMhMoPGi2tM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rw9D6/btsCTBn2lQ0/S0kHqRy5k9kMhMoPGi2tM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rw9D6/btsCTBn2lQ0/S0kHqRy5k9kMhMoPGi2tM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frw9D6%2FbtsCTBn2lQ0%2FS0kHqRy5k9kMhMoPGi2tM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;837&quot; height=&quot;579&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;비동기는 A가 함수 B를 호출한 뒤, A는 다른 일을 하거나 멈춰있고 B가 결과값을 반환해 주면 처리를 할 수도, 안 할 수도 있는 방식입니다. 즉, 결괏값에 대해서 신경 쓰지 않습니다. 이 또한 동기 방식과 마찬가지로 다른 일을 하거나 멈춰있는&amp;nbsp;것에 대해서는 blocking이냐 non-blocking이냐에 따라 다릅니다. blocking 방식의 경우 멈춰있고, B의 작업이 끝나면 A는 자신의 작업을 계속할 수도 있고 반환된 값으로 작업을 할 수도 있습니다. 반면에 non-blocking의 경우에는 B가 작업하는 동안에 A는 자신의 작업을 하지만, 동기 방식과 달리 결괏값에 관심이 없기 때문에 B에게 물어보는 과정이 없습니다. B는 결괏값을 반환해 주면 A는 그 결괏값을 가지고 작업을 할 수도 있고 안 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4가지 조합&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sync-Blocking&lt;/h3&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;665&quot; data-origin-height=&quot;697&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnMRrk/btsCOAX0piW/rbBMKq0wMPSDjVneMPEYq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnMRrk/btsCOAX0piW/rbBMKq0wMPSDjVneMPEYq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnMRrk/btsCOAX0piW/rbBMKq0wMPSDjVneMPEYq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnMRrk%2FbtsCOAX0piW%2FrbBMKq0wMPSDjVneMPEYq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;697&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;697&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Sync(동기)이므로 A는 B의 리턴값을 필요로 합니다. Blocking 방식이므로 A함수가 B를 호출하면 제어권은 B로 넘어가게 되면서 A는 다른 작업을 할 수 없습니다. 이후 B함수에서 작업을 완료하면 반환된 리턴값과 제어권을 갖고 A에서는 작업을 재개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sync-NonBlocking&lt;/h3&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;654&quot; data-origin-height=&quot;719&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt5VSg/btsCRfsiafu/6e7gGLbWqPILNgoEqVPkJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt5VSg/btsCRfsiafu/6e7gGLbWqPILNgoEqVPkJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt5VSg/btsCRfsiafu/6e7gGLbWqPILNgoEqVPkJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt5VSg%2FbtsCRfsiafu%2F6e7gGLbWqPILNgoEqVPkJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;719&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;719&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Sync(동기)이므로 A는 B의 리턴값을 필요로 합니다. NonBlocking 방식이므로 A함수는 제어권을 그대로 유지하고 있어 작업을 수행할 수 있기 때문에 다른 작업을 하면서 중간 중간 B에게 완료되었는지 확인하는 요청을 보냅니다. B의 실행이 완료되었다면 결괏값을 가져와서 해당 결괏값에 대한 처리를 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Async-Blocking&lt;/h3&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;760&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caY4Fw/btsCMyl3uqY/xkYdlk91PyGzn3we8TKNxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caY4Fw/btsCMyl3uqY/xkYdlk91PyGzn3we8TKNxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caY4Fw/btsCMyl3uqY/xkYdlk91PyGzn3we8TKNxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaY4Fw%2FbtsCMyl3uqY%2FxkYdlk91PyGzn3we8TKNxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;748&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Async(비동기)이므로 A는 B에게 콜백함수를 전달하고, A는 B의 리턴값을 신경쓰지 않습니다. Blocking 방식이므로 B가 실행되면 제어권은 B에게 넘어가서 A는 작업을 할 수 없습니다. 콜백함수가 호출되어 제어권이 A로 넘어가면 A는 결괏값에 대한 처리 작업을 할 수도 있고, 안 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Async-NonBlocking&lt;/h3&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;647&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cR4uBB/btsCU17KeVV/YtHEqEgeDGrN50JyCvRohK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cR4uBB/btsCU17KeVV/YtHEqEgeDGrN50JyCvRohK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cR4uBB/btsCU17KeVV/YtHEqEgeDGrN50JyCvRohK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcR4uBB%2FbtsCU17KeVV%2FYtHEqEgeDGrN50JyCvRohK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;701&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Async(비동기)이므로 A는 B에게 콜백함수를 전달하고, A는 B의 리턴값을 신경쓰지 않습니다. NonBlocking 방식이므로 B가 실행되어도 A는 제어권을 갖고 있어 작업을 계속 진행합니다. B는 자신의 작업이 끝나면 콜백함수로 A에게 작업이 끝났음을 알리지만, A는 이에 대한 처리 작업을 할 수도 있고, 안 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기와 비동기의 차이는 &lt;b&gt;결과값을 반환했을 때, 순서와 결괏값에 대해 관심이 있는지 없는지&lt;/b&gt; 로 판단할 수 있습니다. 예를 들어, A함수가 B함수를 호출했을 때 동기방식의 경우 A함수는 B의 결괏값에 관심이 있기 때문에 B함수가 결괏값을 반환해 줘야만 그에 대한 처리를 할 수 있습니다. B 함수가 작업을 처리하는 동안에 A가 작업을 할 수 있는지, 없는지는 Blocking 방식이냐 Non-blocking 방식이냐에 따라 다릅니다. 비동기 방식에서는 A함수가 B함수의 결괏값에 관심이 없기 때문에 B함수가 작업이 끝나면 결괏값에 대한 처리할 수도 있고 안 할 수도 있습니다. 여기서도 마찬가지로 B함수가 작업을 처리하는 동안 A가 작업을 할 수 있는지, 없는지는 Blocking 방식이냐 Non-blocking 방식이냐에 따라 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=oEIoqGd-Sns&amp;amp;list=PLo0ta52hn1uHQ5iQ3hAeRoMUeLJFIeRew&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; [10분 테코톡]   멍토의 Blocking vs Non-Blocking, Sync vs Async&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://velog.io/@nittre/%EB%B8%94%EB%A1%9C%ED%82%B9-Vs.-%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9-%EB%8F%99%EA%B8%B0-Vs.-%EB%B9%84%EB%8F%99%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 블로킹 Vs. 논블로킹, 동기 Vs. 비동기&lt;/a&gt;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>Async</category>
      <category>blocking</category>
      <category>CS</category>
      <category>Non blocking</category>
      <category>Sync</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/50</guid>
      <comments>https://backtony.tistory.com/50#entry50comment</comments>
      <pubDate>Sat, 30 Dec 2023 20:49:03 +0900</pubDate>
    </item>
    <item>
      <title>리눅스 메모리 관리</title>
      <link>https://backtony.tistory.com/49</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리가 관리되는 방법&lt;/h2&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;1117&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEm9A2/btsCSaEkBKE/KB6q9oeGEFngKvbfOJiSwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEm9A2/btsCSaEkBKE/KB6q9oeGEFngKvbfOJiSwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEm9A2/btsCSaEkBKE/KB6q9oeGEFngKvbfOJiSwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEm9A2%2FbtsCSaEkBKE%2FKB6q9oeGEFngKvbfOJiSwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1117&quot; height=&quot;528&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메모리는 주소 덩어리로, 주소로 인덱싱하는 커다란 배열입니다. 컴퓨터가 부팅되면 텅텅 비어있던 메모리에 운영체제나 사용자 프로그램이 배열의 원소처럼 채워지면서 CPU를 점유할 기회를 노립니다. CPU가 메모리에 채워진 프로그램 속 코드를 곧장 읽으면 좋겠지만 CPU를 코드를 읽지 못합니다. 숫자로 바꿔줘야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfZqTM/btsCMrtOdGn/YI2pgvkbh91Kj7gHRdaN90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfZqTM/btsCMrtOdGn/YI2pgvkbh91Kj7gHRdaN90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfZqTM/btsCMrtOdGn/YI2pgvkbh91Kj7gHRdaN90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfZqTM%2FbtsCMrtOdGn%2FYI2pgvkbh91Kj7gHRdaN90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;279&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;소스 코드를 숫자로 바꿔주는 것이 컴파일러고, 컴파일러가 동작하는 과정에서 코드들의 논리 주소를 결정합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EGndd/btsCOEMYHZJ/BKhEJa72ROaDv4ATravi0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EGndd/btsCOEMYHZJ/BKhEJa72ROaDv4ATravi0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EGndd/btsCOEMYHZJ/BKhEJa72ROaDv4ATravi0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEGndd%2FbtsCOEMYHZJ%2FBKhEJa72ROaDv4ATravi0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;972&quot; height=&quot;494&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;각각 프로그램마다 다른 논리 주소를 갖는 것이 아니라, 중복되는 논리 주소를 갖고 있습니다. 그래서 논리 주소를 가상 주소라고도 부릅니다. 모두 같은 주소를 사용한다면 메모리에서 어떻게 이를 구분할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;386&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YdFpD/btsCSzRwYNy/4VnbxmwSJqbxv1kgVxX3O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YdFpD/btsCSzRwYNy/4VnbxmwSJqbxv1kgVxX3O0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YdFpD/btsCSzRwYNy/4VnbxmwSJqbxv1kgVxX3O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYdFpD%2FbtsCSzRwYNy%2F4VnbxmwSJqbxv1kgVxX3O0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;386&quot; height=&quot;521&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;논리 주소 앞에 하나의 주소값이 더 추가되면서 프로그램마다 독립적인 주소가 생기고 이를 물리 주소라고 합니다. 그렇다면 논리 주소에 추가적으로 주소를 붙여서 물리 주소를 만들 필요 없이 심볼릭 주소에서 곧장 물리 주소로 만들면 되지 않을까 생각할 수 있습니다. 이유는 CPU가 논리 주소만을 읽기 때문입니다. CPU는 현재 활동 중인 프로세스 안의 내부 주소만 알면 되지 어떤 프로세스인지는 알 필요가 없습니다. CPU는 논리 주소만으로 물리 메모리에 올라와있는 프로세스들의 정보를 읽는데 어떤 프로세스인지도 모르는데 정보를 읽는 게 어떻게 가능할까요? 운영체제가 도와준다고 생각할 수 있지만, 운영체제도 메모리에 올라와 동작하는 프로세스 중 하나일 뿐입니다. 똑같이 CPU에게 논리 주소로 정보를 읽히는 입장 합니다. 즉, 소프트웨어적으로는 물리 주소를 찾을 수 있는 방법이 없습니다. 그래서 하드웨어적인 도움이 필요합니다. 그 도움을 주는 것이 MMU(Memory Management Unit)입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xk4LH/btsCRfeJjMG/Tpb2Rcy2kim2FbUGBGFhw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xk4LH/btsCRfeJjMG/Tpb2Rcy2kim2FbUGBGFhw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xk4LH/btsCRfeJjMG/Tpb2Rcy2kim2FbUGBGFhw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxk4LH%2FbtsCRfeJjMG%2FTpb2Rcy2kim2FbUGBGFhw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1079&quot; height=&quot;557&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MMU는 프로그램의 시작 주소를 갖는 Base register, 마지막 주소를 갖는 Limit register, 간단한 산술 연산기로 이뤄져 있습니다. CPU를 사용중인 프로세스가 요청하는 논리 주소에 Base register를 더해서 물리 주소로 변환시켜서 완성된 물리 주소로 메모리에 프로세스가 가진 정보를 정확하게 읽어올 수 있게 됩니다. 이렇게 동작하기 전에 선행 동작으로, Limit register에 들어있는 마지막 주소로 현재 요청하는 논리 주소가 올바른지 확인하는 작업을 합니다. 만약 Limit register를 넘어가는 주소를 요청하게 되면 해당 프로세스를 멈추고 CPU권한을 운영체제에 넘깁니다. 운영체제는 이 프로세스가 왜 멈췄는지 살펴보고, 악의적이었다면 바로 응징을 합니다.&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;1242&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bihxZS/btsCOEsGJ6C/M7CumEBDdVv4oGkle99mv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bihxZS/btsCOEsGJ6C/M7CumEBDdVv4oGkle99mv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bihxZS/btsCOEsGJ6C/M7CumEBDdVv4oGkle99mv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbihxZS%2FbtsCOEsGJ6C%2FM7CumEBDdVv4oGkle99mv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1242&quot; height=&quot;514&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메모리에 프로세스들이 차례대로 채워지고 MMU를 통해 고정된 주소를 한번씩 더하면서 물리 메모리를 참조하니 메모리 사용이 간단해 보이지만, 실제로는 메모리에 프로세스들이 딱 맞춰서 채워지지 않습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6y4rx/btsCQqANwrs/eBxsM4EuvdAv6bY0DXOqK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6y4rx/btsCQqANwrs/eBxsM4EuvdAv6bY0DXOqK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6y4rx/btsCQqANwrs/eBxsM4EuvdAv6bY0DXOqK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6y4rx%2FbtsCQqANwrs%2FeBxsM4EuvdAv6bY0DXOqK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;529&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프로세스들이 들어갔따 나가면서 그림처럼 빈 공간이 생기고, 어느 시점에는 빈 공간을 합치면 들어갈 수 있지만, 빈 공간이 연결되어 있지 않아서 프로세스가 들어가지 못하는 상황이 생깁니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;783&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uPR33/btsCMzL1mnP/VArtsfPDwmsCf2U7NBgiu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uPR33/btsCMzL1mnP/VArtsfPDwmsCf2U7NBgiu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uPR33/btsCMzL1mnP/VArtsfPDwmsCf2U7NBgiu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuPR33%2FbtsCMzL1mnP%2FVArtsfPDwmsCf2U7NBgiu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;783&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;783&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메모리가 꽉차서 들어갈 수 없는 상태에서 수강 신청 같이 급하게 필요한 경우라면 당장 불필요한 강의 영상을 내리고 수강 신청을 메모리에 올립니다. 프로세스를 일시적으로 메모리에서 하드디스크에 있는 swap 공간으로 내쫓는 것이 swapping 기법입니다. 하지만 Swapping 기법이 만능은 아닙니다. Swapping 할 프로세스를 고르는 것도 일이고, 우선순위를 판단하는 것도 일입니다. 또한, 하드디스크까지 전체 프로세스를 옮기는데 상대적으로 많은 시간이 소요됩니다. 이에 대한 해결책이 여러 가지 제시되었지만, 결국에는 메모리 공간을 일정하게 잘라두고 그에 맞춰 프로그램을 조금씩 잘라서 올리자는 결론에 이릅니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ueY3R/btsCUACrH8o/yCKiXwhwUL17OmtQuQXpV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ueY3R/btsCUACrH8o/yCKiXwhwUL17OmtQuQXpV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ueY3R/btsCUACrH8o/yCKiXwhwUL17OmtQuQXpV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FueY3R%2FbtsCUACrH8o%2FyCKiXwhwUL17OmtQuQXpV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;977&quot; height=&quot;515&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프로그램을 조금씩 잘라서 올리기 위해 물리 메모리를 동일한 크기로 잘랐습니다. 이 공간들을 Frame이라고 부릅니다. 그리고 프로그램들을 Frame과 동일한 크기로 자르고 잘린 것 하나를 Page라고 부릅니다. 여기서 당장 프로그램이 동작하는데 필요한 최소한의 Page들만 메모리에 올리고 나머지는 Swap 공간에 저장해둡니다. 이것이 Paging 기법입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kAlcb/btsCN2UQ0xt/UwaM12FM6SsCRekCKAkPu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kAlcb/btsCN2UQ0xt/UwaM12FM6SsCRekCKAkPu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kAlcb/btsCN2UQ0xt/UwaM12FM6SsCRekCKAkPu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkAlcb%2FbtsCN2UQ0xt%2FUwaM12FM6SsCRekCKAkPu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;352&quot; height=&quot;535&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Paging 기법 덕분에 메모리에 낭비되는 구멍은 거의 없어졌지만, 한 프로그램의 페이지가 여기저기 분포되면서 순서도 보장할 수 없게 되어 MMU의 계산이 복잡해지게 됩니다. 그렇다면 순서도 보장않되고 복잡해진 페이지들을 어떻게 조회할까요?&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7wqIQ/btsCRhDDaEQ/GkclWLv6RmXydFRRcZGCs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7wqIQ/btsCRhDDaEQ/GkclWLv6RmXydFRRcZGCs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7wqIQ/btsCRhDDaEQ/GkclWLv6RmXydFRRcZGCs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7wqIQ%2FbtsCRhDDaEQ%2FGkclWLv6RmXydFRRcZGCs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;911&quot; height=&quot;545&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;논리, 물리 주소 변환을 위한 별도의 페이지 테이블을 사용합니다. 페이지 테이블 때문에 MMU 레지스터의 이름과 용도도 달라지게 됩니다. 이전에 프로세스의 시작 주소를 더해주던 Base Register는 페이지 테이블의 시작 주소를 더해주는 Page Table Base Register로 변경되었고, 프로세스의 마지막 주소를 검증하던 Limig register는 Page Table의 크기를 검증하는 Page Table Length Register로 변경되었습니다. 페이지 테이블에는 물리 메모리에 있는지 Swap공간에 있는지 빨리 검사하기 위해 Valid 비트도 추가되었습니다.&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;939&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYvKGx/btsCMocMPvC/y0TDdz4gdcuMkqQDdkxZo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYvKGx/btsCMocMPvC/y0TDdz4gdcuMkqQDdkxZo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYvKGx/btsCMocMPvC/y0TDdz4gdcuMkqQDdkxZo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYvKGx%2FbtsCMocMPvC%2Fy0TDdz4gdcuMkqQDdkxZo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;939&quot; height=&quot;543&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;CPU가 MMU에게 논리적인 주소로 요청하게 되면 계산이 끝난 값으로 페이지 테이블을 참조해서 찾아낸 Frame 주소로 이동해 해당 페이지의 주소를 읽어냅니다. 그렇다면 이 페이지 테이블은 어디에 저장될까요? 페이지 테이블은 메모리에 저장됩니다. 우선 페이지 테이블의 행 개수는 해당 프로세스를 일정한 간격으로 나눈 수입니다. 프로세스마다 다르겠지만 페이지 테이블의 행이 100만 개가 넘기는 경우가 대다수인데 이것이 프로세스마다 한 개씩 존재합니다. 메모리 공간을 효율적으로 사용하기 위해 페이징 기법을 적용했는데 페이징 테이블이 공간을 사용하고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxJzny/btsCQ2T51Uc/y2o8zTaCZqDJFzZBxm1vH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxJzny/btsCQ2T51Uc/y2o8zTaCZqDJFzZBxm1vH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxJzny/btsCQ2T51Uc/y2o8zTaCZqDJFzZBxm1vH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxJzny%2FbtsCQ2T51Uc%2Fy2o8zTaCZqDJFzZBxm1vH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;730&quot; height=&quot;531&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;공간을 최대한 아껴보고자, 프로세스들끼리 공통적으로 사용하는 부분은 메모리에 한개씩만 올리고 프로세스뜰이 나눠 쓰게 만들고 그것을 Shared Page라고 합니다. 이는 절대 수정되면 안 되므로 Read Only 권한이 부여되고, 별도의 탐색 없이 쉽게 찾을 수 있도록 서로 동일한 논리주소에 위치합니다. Read Only 권한을 표시하기 위해서 페이지 테이블에 Auth 비트가 추가적으로 생깁니다.&lt;br /&gt;이로써 페이지 테이블을 메모리에 저장한 만큼의 공간을 다시 확보했지만 이번에는 속도가 발복을 잡습니다. 페이지 테이블을 메모리에 위치하고 페이지도 메모리에 위치합니다. CPU가 정보를 요청할 때마다 페이지 테이블에 접근하고 좌표를 받아서 다시 메모리에 접근해서 데이터를 가져오면서 결국 메모리에 2번씩 접근해야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cu0usU/btsCUZ9U4PG/B04JE6DIQCjj4ggUxau511/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cu0usU/btsCUZ9U4PG/B04JE6DIQCjj4ggUxau511/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cu0usU/btsCUZ9U4PG/B04JE6DIQCjj4ggUxau511/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcu0usU%2FbtsCUZ9U4PG%2FB04JE6DIQCjj4ggUxau511%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1386&quot; height=&quot;549&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이를 해결하기 위해 추가적인 하드웨어 TLB(Translation Look-aside Buffers)의 도움을 받습니다. 페이지 테이블을 보기전에 한번 확인하는 캐시 메모리입니다. CPU가 논리 주소로 정보를 요청하면 페이지 테이블에 접근하기 전 우선 TLB부터 확인합니다.&lt;br /&gt;TLB에 매칭된 주소가 있으면 TLB에 있는 Frame 주소로 변환해서 바로 메모리에서 데이터를 가져옵니다. TLB에 없다면 어쩔수없이 2번 메모리에 접근하게 됩니다. 대부분의 프로세스는 한번 참조했던 곳을 다시 참조할 가능성이 매우 높으므로 TLB의 성공 확률이 높아서 거의 1번의 메모리 접근으로 끝나게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해 보자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현대 메모리는 페이징을 베이스로 한 기법을 채택&lt;/li&gt;
&lt;li&gt;하드디스크를 Swap 공간으로 활용하여 잉여 페이지들을 보관&lt;/li&gt;
&lt;li&gt;논리 주소를 물리 주소로 변환하기 위해서 MMU, TLB 같은 하드웨어들의 지원을 받아 Page Table을 확인하고 메모리를 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리눅스가 메모리를 관리하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 이야기한 페이징 기법에서 운영체제는 2가지 일을 하고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가상 메모리로 사용자 프로세스 속이기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S6tEB/btsCVGoRSCY/u8KV2uzWMQKzkR1E1FKxGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S6tEB/btsCVGoRSCY/u8KV2uzWMQKzkR1E1FKxGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S6tEB/btsCVGoRSCY/u8KV2uzWMQKzkR1E1FKxGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS6tEB%2FbtsCVGoRSCY%2Fu8KV2uzWMQKzkR1E1FKxGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;798&quot; height=&quot;625&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;CPU를 점유하고 있는 프로세스는 자신이 온전하게 전부 메모리에 올라와 있다고 생각합니다.&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;1261&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Hvib/btsCQpBS5e8/HPc2PVxD3jRkJmcDMpfNk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Hvib/btsCQpBS5e8/HPc2PVxD3jRkJmcDMpfNk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Hvib/btsCQpBS5e8/HPc2PVxD3jRkJmcDMpfNk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Hvib%2FbtsCQpBS5e8%2FHPc2PVxD3jRkJmcDMpfNk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1261&quot; height=&quot;600&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 실제로는 동작에 필요한 부분만 물리 메모리, 나머지는 swap 공간에 저장되어 있습니다.&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;942&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJMgAV/btsCMyM8uhT/B7p6XdNMqNKBdKVIUNpv70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJMgAV/btsCMyM8uhT/B7p6XdNMqNKBdKVIUNpv70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJMgAV/btsCMyM8uhT/B7p6XdNMqNKBdKVIUNpv70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJMgAV%2FbtsCMyM8uhT%2FB7p6XdNMqNKBdKVIUNpv70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;942&quot; height=&quot;412&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;물리 메모리 공간과 Swap 공간을 합쳐서 만들어낸 가짜 메모리를 가상 메모리라고 합니다. 페이징 기법 중 CPU를 통해서 요구하던 논리 주소가 사실 가상 메모리 상의 주소 가상 주소였던 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하드디스크의 입출력(I/O) 장치 관리&lt;/h3&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;1413&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq2EEd/btsCSaqMoDX/9fRzizWnRfoGHbS6gKa2a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq2EEd/btsCSaqMoDX/9fRzizWnRfoGHbS6gKa2a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq2EEd/btsCSaqMoDX/9fRzizWnRfoGHbS6gKa2a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq2EEd%2FbtsCSaqMoDX%2F9fRzizWnRfoGHbS6gKa2a1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1413&quot; height=&quot;404&quot; data-origin-width=&quot;1413&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;주소를 변환하고 메모리에서 페이지를 찾아내는 것은 사용자 프로세스와 하드웨어에서 진행하지만, 하드디스크 같이 입출력 저장장치를 건드리는 것은 운영체제 관할입니다. 즉, Swap 공간에서 페이지를 꺼내려면 운영체제의 도움이 필요합니다. 프로세스가 CPU를 점유하고 한참 작업을 이어나가던 도중에 TLB에 메모리에 없는 페이지를 요구합니다. 메모리에 페이지가 없다는 것을 알아차린 MMU가 프로세스를 일시정지 시킵니다. 운영체제가 CPU를 점유하고 왜 프로세스가 멈췄는지 체크합니다. 만약 이상한 주소를 요청했다면 바로 차단하고, 아니라면 운영체제가 하드디스크의 swap공간에서 페이지를 메모리로 가져오고 TLB에 주소를 등록과 페이지 테이블에도 Valid 비트와 함께 업데이트합니다. 그리고 운영체제는 CPU를 내려놓고 다시 빠집니다. 그런데 Swap 공간에서 페이지를 가져오기까지 시간이 매우 길기 때문에 중간에 다른 프로세스에게 CPU가 넘어갈 수 있습니다. 이런 경우 해당 프로세스는 대기 큐에 들어가서 다시 자기 차례를 기다립니다. 이런 경우가 아니라면 운영체제가 CPU를 내려놓으면 기존 프로세스가 CPU를 차지하고 명령수행을 실패한 지점부터 다시 동작을 수행하게 됩니다. 즉, Page Fault가 발생하면 CPU가 다른 프로세스로 넘어갈 만큼 많은 시간이 소모되는 것을 알 수 있습니다. 따라서, Page Fault 확률이 곧 성능이 됩니다. 하지만, 컴퓨터 프로그램의 특성상 중복된 내용 참조가 많아서 Page Fault 확률이 낮고 대부분 TLB를 참조하면서 빠르게 작업이 진행됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPyOLB/btsCSaxBlXD/fxtsWf5OTo4iFpt9Nc986K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPyOLB/btsCSaxBlXD/fxtsWf5OTo4iFpt9Nc986K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPyOLB/btsCSaxBlXD/fxtsWf5OTo4iFpt9Nc986K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPyOLB%2FbtsCSaxBlXD%2FfxtsWf5OTo4iFpt9Nc986K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;503&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;물리 메모리에 프레임이 가득 차게 된다면, 메모리를 차지한 페이지 하나를 내쫓아야 합니다. 이 행위를 Page Replacement라고 하고 어떤 페이지를 교체할지는 운영체제가 결정합니다. 쫓아낼 페이지를 선정하는 방법에는 LRU, 마지막으로 참조된 시점이 가장 오래된 페이지를 찾아내는 알고리즘이 적합해 보입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xZUxx/btsCOzkr4gr/sDzb1kKrPOKnOcL6CNDw41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xZUxx/btsCOzkr4gr/sDzb1kKrPOKnOcL6CNDw41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xZUxx/btsCOzkr4gr/sDzb1kKrPOKnOcL6CNDw41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxZUxx%2FbtsCOzkr4gr%2FsDzb1kKrPOKnOcL6CNDw41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;642&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 실제 운영체제에서는 LRU 방식이 사용되지 않습니다. 이유는 메모리에 데이터가 이미 존재한 경우, Page Fault가 나지 않기 때문에 운영체제가 개입하지 않기 때문입니다. 따라서 운영체제는 자신이 관리했던 Page Fault만을 기억하게 되고, 다른 페이지들은 언제 접근되었고 몇 번이나 사용되었는지는 알 수 없습니다. 즉, LRU에 필요한 정보를 절반만 알고 있게 되어 LRU를 사용할 수 없습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/booL4P/btsCRg5Mndd/eG4a5SpubKirDPjMlHKQTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/booL4P/btsCRg5Mndd/eG4a5SpubKirDPjMlHKQTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/booL4P/btsCRg5Mndd/eG4a5SpubKirDPjMlHKQTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbooL4P%2FbtsCRg5Mndd%2FeG4a5SpubKirDPjMlHKQTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1026&quot; height=&quot;615&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;대신에 LRU 계열인 Clock Algorithm을 사용합니다. 메모리에 올라와 있는 모든 페이지마다 1개의 reference bit를 갖게 합니다. 초기에는 모두 0이고 CPU를 점유하고 있는 프로세스로부터 참조되면 bit가 1로 올라갑니다. 이 상태에서 페이지 교환이 이뤄질 경우, 한쪽 방향으로 페이지 테이블을 참조하기 시작합니다. 참조하는 과정에서 1비트를 만나면 0으로 바꾸고, 0비트를 만나면 그것이 교환의 대상이 됩니다. 가장 오래되지 않은 페이지를 잡아낼 수는 없지만, 가장 최근에 참조된 페이지는 피할 수 있게 되는 것입니다. 이 reference bit은 페이지 테이블에 추가됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clock 알고리즘으로 선택한 페이지를 쫓아내야 하는데 이도 함부로 쫓아낼 수는 없습니다. CPU를 점유한 프로세스로부터 참조되는 동안 변경사항이 있는지 확인해야 합니다. 변경사항이 없다면 바로 쫓아내고, 변경사항이 있다면 하드디스크에도 변경된 내용을 반영합니다. 그럼 변경사항은 어떻게 감지할까요?&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;931&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/smsdl/btsCSDl85DL/gNLjkvmT2u5Y8JojmGcTf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/smsdl/btsCSDl85DL/gNLjkvmT2u5Y8JojmGcTf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/smsdl/btsCSDl85DL/gNLjkvmT2u5Y8JojmGcTf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsmsdl%2FbtsCSDl85DL%2FgNLjkvmT2u5Y8JojmGcTf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;931&quot; height=&quot;307&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;페이지 테이블에 dirty 비트가 하나 더 추가됩니다. 즉, 하드디스크에 변경사항을 반영하고, 반영되었으니 페이지 테이블의 Dirty 비트를 수정하는 것도 운영체제가 수행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Trashing(쓰레싱)&lt;/h3&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;826&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4vL0F/btsCSCHwCpf/T3uasCIi0dJHkKGdYjLYZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4vL0F/btsCSCHwCpf/T3uasCIi0dJHkKGdYjLYZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4vL0F/btsCSCHwCpf/T3uasCIi0dJHkKGdYjLYZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4vL0F%2FbtsCSCHwCpf%2FT3uasCIi0dJHkKGdYjLYZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;826&quot; height=&quot;649&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Page Fault, Page Replacement가 발생하면서 다양한 프로세스가 메모리에 올라오면 메모리의 유효공간은 줄어들고 CPU의 가동시간이 올라가면서 자원을 최대한 활용하는 상태에 이릅니다. 하지만 시간이 흐르면 CPU 사용률이 떨어지게 되는데 이는 메모리에 프로세스가 많아지면서 프로세스당 물리 메모리를 사용할 수 있는 프레임의 개수가 줄어들어 페이지가 물리 메모리에 적게 올라온 프로세스는 명령을 조금만 수행해도 Page Fault가 발생하여 Page Replacement를 진행하게 되기 때문입니다. Page Replacement로 Swap 공간에서 페이지를 가져오기까지 상대적으로 오랜 시간이 걸리기 때문에 그동안 다른 프로세스가 CPU를 넘겨받지만 그 프로세스도 곧 Page Replacement를 진행하게 됩니다. 결과적으로 모든 프로세스들이 페이지를 교체하느라 바쁜 반면에, CPU는 할 일이 없어서 쉬게 되는데 CPU가 놀고 있는 것을 발견한 운영체제는 더 많은 프로세스를 메모리에 올리면서 악순환이 반복됩니다. 이 현상을 Trashing이라고 합니다. Trashing을 해소하기 위해 운영체제는 Working Set 알고리즘과 Page Fault Frequency 알고리즘을 사용합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btXFWG/btsCN1azJr0/UcCQR6pm9CFSEHcKoeZdyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btXFWG/btsCN1azJr0/UcCQR6pm9CFSEHcKoeZdyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btXFWG/btsCN1azJr0/UcCQR6pm9CFSEHcKoeZdyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtXFWG%2FbtsCN1azJr0%2FUcCQR6pm9CFSEHcKoeZdyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;792&quot; height=&quot;655&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Working Set 알고리즘은 대부분의 프로세스가 일정한 페이지만 집중적으로 참조한다는 성격을 이용해서 특정 시간 동안 참조되는 페이지 개수를 파악하여 그 페이지 개수만큼 프레임이 확보되면 그때 페이지들을 메모리에 올리는 알고리즘입니다. Page Replacement 활동을 진행할 때도 프로세스마다 Working Set 단위로 페이지를 쫓아냅니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/taSLE/btsCUX5kuPS/RzzVBhTS8fU7KARFGYQ2L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/taSLE/btsCUX5kuPS/RzzVBhTS8fU7KARFGYQ2L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/taSLE/btsCUX5kuPS/RzzVBhTS8fU7KARFGYQ2L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtaSLE%2FbtsCUX5kuPS%2FRzzVBhTS8fU7KARFGYQ2L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1015&quot; height=&quot;560&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Page Fault Frequency 알고리즘은 Page Fault 퍼센트의 상한과 하한을 두고 상한을 넘으면 지급하는 프레임 개수를 늘리고, 하한을 넘으면 지급 프레임 개수를 줄입니다. 이도 남는 프레임이 없으면 프로세스 단위로 페이지를 쫓아냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 고갈 상황과 CPU 사용률을 체크하는 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리가 고갈되면 어떤 상황이 발생할까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스들의 Swap이 활발해지면서 CPU 사용률이 하락하게 됩니다. 운영체제는 CPU 사용률이 하락한 것을 보고 프로세스를 추가하게 되어 Trashing 현상이 발생합니다. Trashing 현상이 해결되지 않을 경우 Out Of Memory 상태로 판단되어 중요도가 낮은 프로세스를 찾아 강제로 종료하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU 사용률을 계속 체크해야 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시점만 체크한 경우 CPU 사용률이 높아 보일 수 있습니다. 하지만 연속적으로 체크하게 되면 CPU 사용률이 급격하게 떨어지는 구간을 발견할 가능성이 높아집니다. 이때 메모리 적재량을 함께 체크하면 Trashing의 발생 유무도 확인할 수 있게 됩니다.&lt;br /&gt;따라서 Trashing이 발견되었다면 서버자원을 추가적으로 배치하는 등 해결방안을 마련할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=qxmdX449z1U&amp;amp;list=PLo0ta52hn1uHQ5iQ3hAeRoMUeLJFIeRew&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; [10분 테코톡]  &amp;zwj;♂️ 현구막의 리눅스 메모리 관리&lt;/a&gt;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>CS</category>
      <category>리눅스</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/49</guid>
      <comments>https://backtony.tistory.com/49#entry49comment</comments>
      <pubDate>Sat, 30 Dec 2023 20:31:53 +0900</pubDate>
    </item>
    <item>
      <title>Process &amp;amp; Thread</title>
      <link>https://backtony.tistory.com/48</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로그램과 프로세스&lt;/h2&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;731&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLs83h/btsCU10YAAh/oM1PkKo8X8Lx8IEDA8Z1D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLs83h/btsCU10YAAh/oM1PkKo8X8Lx8IEDA8Z1D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLs83h/btsCU10YAAh/oM1PkKo8X8Lx8IEDA8Z1D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLs83h%2FbtsCU10YAAh%2FoM1PkKo8X8Lx8IEDA8Z1D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;308&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피자와 피자 레시피를 비유로 들면 이와 같습니다. 피자 레시피 = 코드가 구현되있는 파일 = 프로그램 피자 레시피가 피자가 되는 것처럼 코드 파일(프로그램)도 실행되어 사용할 수 있는 무언가!!가 되어야 합니다. 그 무언가가 &lt;b&gt;프로세스&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로그램 -&amp;gt; 프로세스&lt;/h3&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;922&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpM7WY/btsCMxHtn5S/TrBrP8SkYg5P588kqfv1yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpM7WY/btsCMxHtn5S/TrBrP8SkYg5P588kqfv1yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpM7WY/btsCMxHtn5S/TrBrP8SkYg5P588kqfv1yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpM7WY%2FbtsCMxHtn5S%2FTrBrP8SkYg5P588kqfv1yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;922&quot; height=&quot;441&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프로그램이 프로세스가 되면서 총 2가지 일이 발생합니다. 프로세스가 필요로 하는 재료들이 메모리에 올라가야 합니다. 메모리는 Code, Data, Heap, Stack 으로 총 4가지 영역으로 구성되어 있습니다. 또한, 해당 프로세스에 대한 정보를 담고 있는 Process Control Block(PCB)가 프로세스 생성 시 함께 만들어 집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Process &amp;amp; Thread&lt;/h2&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;754&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/43pB9/btsCSbiVmHf/J8rhMI0zKqhhUtcvKLrGr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/43pB9/btsCSbiVmHf/J8rhMI0zKqhhUtcvKLrGr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/43pB9/btsCSbiVmHf/J8rhMI0zKqhhUtcvKLrGr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F43pB9%2FbtsCSbiVmHf%2FJ8rhMI0zKqhhUtcvKLrGr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;754&quot; height=&quot;396&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 대부분 위 그림과 같이 여러 가지의 프로세스를 동시에 사용합니다. 하지만 원래 한 프로세스가 실행되기 위해서 CPU를 점유하고 있으면 다른 프로세스는 실행상태에 있을 수 없습니다. 노래 듣다가 코딩을 하기 위해서 인텔리제이를 키면 노래가 꺼지게 되는 것입니다. 그래서 &lt;b&gt;다수의 프로세스를 동시에 실행하기 위해 여러 개의 프로세스를 시분할&lt;/b&gt; 로, 즉 짧은 텀을 반복하면서 전환해서 실행을 시키도록 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3BRJ4/btsCQqt1WfP/fLX92iUoCtYzuJOnOn8cjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3BRJ4/btsCQqt1WfP/fLX92iUoCtYzuJOnOn8cjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3BRJ4/btsCQqt1WfP/fLX92iUoCtYzuJOnOn8cjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3BRJ4%2FbtsCQqt1WfP%2FfLX92iUoCtYzuJOnOn8cjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1217&quot; height=&quot;419&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 그림을 화살표 방향대로 보시면 처음에는 PCB_1이 실행상태로 CPU에 적재되고, PCB_2가 실행상태로 되기 위해서는 PCB_1이 다시 준비 상태로 내려가게 됩니다. 이런 일련의 과정을 매우 짧은 텀으로 반복하게 되는 것입니다. 이러한 행위를 &lt;b&gt;컨텍스트 스위칭&lt;/b&gt; 이라고 합니다. 두 개의 프로세스의 컨텍스트 스위칭을 봤는데도 매우 힘든 작업인 것을 알 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Thread&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 등장하는게 경량화된 프로세스 버전인 &lt;b&gt;스레드&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/behNYE/btsCN3sGNyg/inJP6cDyRCQStsA3I6xww0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/behNYE/btsCN3sGNyg/inJP6cDyRCQStsA3I6xww0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/behNYE/btsCN3sGNyg/inJP6cDyRCQStsA3I6xww0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbehNYE%2FbtsCN3sGNyg%2FinJP6cDyRCQStsA3I6xww0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;892&quot; height=&quot;529&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 스레드가 경량화된 프로세스냐 하면, 하나의 프로세스 안에 다수의 스레드가 있다면 &lt;b&gt;공유되는 자원&lt;/b&gt; 이 있기 때문입니다. 스레드는 프로세스의 메모리 구조에서 Code, Data, Heap영역을 공통된 자원으로 사용하고 각 스레드는 Stack 부분만을 따로 갖습니다. 공유되는 자원이 있기 때문에 이전처럼 컨텍스트 스위칭이 일어날 때 캐싱 적중률이 올라갑니다. 즉, 모조리 다 빼고 다시 다 넣을 필요가 없다는 겁니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Multi-process&lt;/h3&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;847&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EbsPh/btsCVFKgleo/49hTt07vsdIDcELRPXXOoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EbsPh/btsCVFKgleo/49hTt07vsdIDcELRPXXOoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EbsPh/btsCVFKgleo/49hTt07vsdIDcELRPXXOoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEbsPh%2FbtsCVFKgleo%2F49hTt07vsdIDcELRPXXOoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;847&quot; height=&quot;537&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;하나의 프로그램을 여러개의 프로세스로 구성하여 각 프로세스가 병렬적으로 작업을 수행하는 것을 의미합니다.&lt;/b&gt; 한 애플리케이션에서 여러 사용자가 로그인을 요청하는 상황이 있다고 가정해 봅니다. 한 프로세스는 매번 하나의 로그인만 처리할 수 있기 때문에 동시에 처리할 수 없습니다. 그래서 부모 프로세스가 fork() 하여 자식 프로세스를 여러 개 만들어 일을 처리하도록 합니다. 이때 자식 프로세스는 부모와 &lt;b&gt;별개의 메모리 영역을 확보&lt;/b&gt; 하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Multi-thread&lt;/h3&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;722&quot; data-origin-height=&quot;539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NVKHl/btsCR9yFqp5/vVr8rQc4cjw1vPXHj5CdoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NVKHl/btsCR9yFqp5/vVr8rQc4cjw1vPXHj5CdoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NVKHl/btsCR9yFqp5/vVr8rQc4cjw1vPXHj5CdoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNVKHl%2FbtsCR9yFqp5%2FvVr8rQc4cjw1vPXHj5CdoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;539&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;539&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;하나의 프로세스에 여러 스레드로 자원을 공유하며 작업을 나누어 수행하는 것을 의미합니다.&lt;/b&gt; &lt;b&gt;스레드는 한 프로세스 내에서 구분지어진 실행 단위입니다.&lt;/b&gt; 만약 프로세스가 다수의 스레드로 구분되어있지 않다면 단일 스레드 하나로 프로세스가 실행됩니다.&lt;br /&gt;이때 실행 단위는 프로세스 그 자체(= 해당 프로세스의 하나밖에 없는 스레드 하나)가 됩니다. 프로세스 내에서 분리해서 여러 스레드로 나뉘어서 실행 단위가 나뉘어지면 Multi-thread가 됩니다. 예시로, 인텔리제이를 사용하면서 테스트도 돌리면서 소스코드를 수정해야 한다면 한 애플리케이션에 대한 작업의 단위가 나뉘게 됩니다. 이때 각각의 스레드가 각 작업을 담당하게 되는 것입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리를 하자면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Multi-process
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 프로세스는 독립적이기에 하나의 프로세스가 비정상 종료되더라고 다른 프로세스에 영향 X&lt;/li&gt;
&lt;li&gt;IPC를 사용한 통신&lt;/li&gt;
&lt;li&gt;자원 소모적, 개별 메모리 차지&lt;/li&gt;
&lt;li&gt;컨텍스트 스위칭 비용이 큼&lt;/li&gt;
&lt;li&gt;동기화 작업이 필요하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Multi-thread
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드끼리 긴밀하게 연결되어 있음&lt;/li&gt;
&lt;li&gt;공유된 자원으로 통신 비용 절감&lt;/li&gt;
&lt;li&gt;공유된 자원으로 메모리가 효율적&lt;/li&gt;
&lt;li&gt;컨텍스트 스위칭 비용이 적음&lt;/li&gt;
&lt;li&gt;공유 자원 관리가 필요함&lt;/li&gt;
&lt;li&gt;동기화 작업이 필요&lt;/li&gt;
&lt;li&gt;하나의 쓰레드가 비정상 종료될 경우, 다른 쓰레드도 종료될 가능성이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Multi-core&lt;/h2&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;884&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLcyMm/btsCOGKLHkA/3PkzAKmbKtrPkKEsvkj2Jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLcyMm/btsCOGKLHkA/3PkzAKmbKtrPkKEsvkj2Jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLcyMm/btsCOGKLHkA/3PkzAKmbKtrPkKEsvkj2Jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLcyMm%2FbtsCOGKLHkA%2F3PkzAKmbKtrPkKEsvkj2Jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;884&quot; height=&quot;186&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글 코어를 가진 CPU가 실행 단위를 처리할 때는 동시에 여러 가지가 진행되기 위해서 빠른 텀으로 전환되면서 실행된다고 앞서 설명했습니다. 이 개념이 &lt;b&gt;동시성&lt;/b&gt; 입니다. 빠르게 여러 실행 단위를 번갈아 실행하면서 동시에 일어난 것처럼 보이게 하는 것입니다. 하지만 멀티 코어는 &lt;b&gt;병렬처리&lt;/b&gt; 합니다. 물리적으로 둘 이상의 코어를 사용해서 동시에 하나 이상의 프로세스(혹은 스레드가)한꺼번에 진행되게 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로세스는 프로세서에 의해 동작하고 있는 프로그램입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로세스가 동작한다는 것은 프로세스의 특정 스레드가 실행 중이라는 의미로 그 특정 스레드는 프로세스가 가진 데이터를 참조합니다.&lt;/li&gt;
&lt;li&gt;따라서 &lt;b&gt;스레드 단위 작업을 지원하기 위한 자원 할당 단위&lt;/b&gt; 라고 말하기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스레드는 한 프로세스 내에서 나뉘어진 하나 이상의 실행 단위이다.&lt;/li&gt;
&lt;li&gt;애플리케이션에 대한 작업을 동시에 하기 위해서는 2가지 처리 방식(멀티 프로세스, 멀티 스레드)이 존재한다.&lt;/li&gt;
&lt;li&gt;동시에 실행되는 것 처럼 보이기 위해서 실행 단위는 시분할로 cpu를 점유하여 컨텍스트 스위칭을 한다.&lt;/li&gt;
&lt;li&gt;멀티 프로세스는 독립적인 메모리를 갖고 있지만, 멀티 스레드는 자원을 공유한다.&lt;/li&gt;
&lt;li&gt;멀티 코어는 하드웨어 측면에서 실행 단위를 병렬적으로 처리할 수 있도록 여러 프로세서가 있는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=1grtWKqTn50&amp;amp;list=PLo0ta52hn1uHQ5iQ3hAeRoMUeLJFIeRew&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; [10분 테코톡]   코다의 Process vs Thread&lt;/a&gt;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>CS</category>
      <category>Process</category>
      <category>Thread</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/48</guid>
      <comments>https://backtony.tistory.com/48#entry48comment</comments>
      <pubDate>Sat, 30 Dec 2023 20:19:17 +0900</pubDate>
    </item>
    <item>
      <title>Spring Data JPA - QueryDSL 사용하기</title>
      <link>https://backtony.tistory.com/47</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle.kts&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id(&quot;org.springframework.boot&quot;) version &quot;3.0.4&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.0&quot;
    kotlin(&quot;jvm&quot;) version &quot;1.7.22&quot;
    kotlin(&quot;plugin.spring&quot;) version &quot;1.7.22&quot;
    kotlin(&quot;plugin.jpa&quot;) version &quot;1.7.22&quot;
    kotlin(&quot;kapt&quot;) version &quot;1.7.22&quot;
}

group = &quot;com.example&quot;
version = &quot;0.0.1-SNAPSHOT&quot;
java.sourceCompatibility = JavaVersion.VERSION_17

allOpen {
    annotation(&quot;jakarta.persistence.Entity&quot;)
    annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
    annotation(&quot;jakarta.persistence.Embeddable&quot;)
}

//noArg {
//    annotation(&quot;jakarta.persistence.Embeddable&quot;)
//    annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
//    annotation(&quot;jakarta.persistence.Entity&quot;)
//}

repositories {
    mavenCentral()
}

dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin&quot;)
    implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)

    // querydsl
    implementation(&quot;com.querydsl:querydsl-jpa:5.0.0:jakarta&quot;)
    kapt(&quot;com.querydsl:querydsl-apt:5.0.0:jakarta&quot;)
    kapt(&quot;jakarta.annotation:jakarta.annotation-api&quot;)
    kapt(&quot;jakarta.persistence:jakarta.persistence-api&quot;)
//    implementation(&quot;org.hibernate.common:hibernate-commons-annotations:6.0.6.Final&quot;)

    implementation(&quot;mysql:mysql-connector-java:8.0.31&quot;)
    runtimeOnly(&quot;com.h2database:h2&quot;)
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
}

tasks.withType&amp;lt;KotlinCompile&amp;gt; {
    kotlinOptions {
        freeCompilerArgs = listOf(&quot;-Xjsr305=strict&quot;)
        jvmTarget = &quot;17&quot;
    }
}

tasks.withType&amp;lt;Test&amp;gt; {
    useJUnitPlatform()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/kotlin_jpa?serverTimezone=UTC&amp;amp;characterEncoding=UTF-8
      username: root
      password: root
      maximum-pool-size: 5
      minimum-idle: 5
      connection-timeout: 5000

  jpa:
    database: mysql
    generate-ddl: true
    database-platform: org.hibernate.dialect.MySQL8Dialect
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    hibernate:
      ddl-auto: create-drop
    open-in-view: false

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;QuerydslConfig&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class QuerydslConfig {
    @PersistenceContext
    private val em: EntityManager? = null

    @Bean
    fun jpaQueryFactory(): JPAQueryFactory {
        return JPAQueryFactory(em)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 문법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 Q-Type 활용&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// 애노테이션 생략
@RequiredArgsConstructor
public class QuerydslBasicTest {
    Private final JPAQueryFactory queryFactory;

    // Querydsl을 사용하기 위해서는 JPAQueryFactory가 필요하다.
    // jpaQueryFactory를 만들때 생성자 파라미터로 EntityManager을 넣어줘야한다.
    public QuerydslBasicTest(EntityManager em){
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Test
    void startquerydsl() throws Exception{
        // compileQuerydsl로 만들어진 QXXX을 사용하여 query를 작성한다.
        // querydsl에서 사용하는 Member을 꺼내온다.
        // 결국 쿼리에서는 m을 기준으로 사용하게 된다.
        // QMember m = QMember.member;

        // 하지만 이것 또한 static import로 줄일 수 있다.       
        // 그냥 쿼리에서 전부 QMember.member하되 
        // Qmember을 static import하게되면 결론적으로 member만으로 사용 가능

        Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq(&quot;member1&quot;)) // 파라미터 바인딩 처리
                .fetchOne();

        assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);

        // 같은 테이블을 조인해야하는경우 별칭이 같으면 안되므로
        // 다른 별칭을 사용해야함
        // member는 그대로 사용하고
        // QMember memberSub = new QMember(&quot;memberSub&quot;);
        // 따로 하나 만들어서 사용
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;compileQuerydsl로 만들어진 Q엔티티 을 사용하여 쿼리 작성합니다.&lt;/li&gt;
&lt;li&gt;Q엔티티는 static import하면 따로 선언 없이 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;파라미터 바인딩을 여러가지로 처리 가능합니다.(eq...)&lt;/li&gt;
&lt;li&gt;같은 테이블을 조인해야 하는 경우 다른 별칭을 주어 사용합니다.&lt;/li&gt;
&lt;li&gt;select와 from이 같은 파라미터를 가지면 selectFrom으로 합칠 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색 조건 쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Querydsl은 JPQL이 제공하는 모든 검색 조건을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;member.username.eq(&quot;member1&quot;) // username = 'member1'
member.username.ne(&quot;member1&quot;) //username != 'member1'
member.username.eq(&quot;member1&quot;).not() // username != 'member1'
member.username.isNotNull() //이름이 is not null
member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30
member.age.goe(30) // age &amp;gt;= 30
member.age.gt(30) // age &amp;gt; 30
member.age.loe(30) // age &amp;lt;= 30
member.age.lt(30) // age &amp;lt; 30
member.username.like(&quot;member%&quot;) //like 검색
member.username.contains(&quot;member&quot;) // like &amp;lsquo;%member%&amp;rsquo; 검색
member.username.startsWith(&quot;member&quot;) //like &amp;lsquo;member%&amp;rsquo; 검색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;// and, or도 가능
Member findMember = queryFactory
 .selectFrom(member)
 .where(member.username.eq(&quot;member1&quot;)
 .and(member.age.eq(10)))
 .fetchOne();

// and -&amp;gt; 쉼표 처리
queryFactory
 .selectFrom(member)
 .where(member.username.eq(&quot;member1&quot;),
        member.age.eq(10))
 .fetch();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where 조건을 엮어줄 때 and()와 or을 사용할 수 있습니다. and의 경우 쉼표(,)도 and로 인식하기 때문에 더 깔끔하게 가져갈 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과 조회&lt;/h3&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;fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환&lt;/li&gt;
&lt;li&gt;fetchOne() : 단 건 조회
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과가 없으면 : null&lt;/li&gt;
&lt;li&gt;결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fetchFirst() : 첫 건만 조회, limit(1).fetchOne() 와 결과 동일&lt;/li&gt;
&lt;li&gt;fetchResults, fetchCount는 deprecated되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
interface FileRepository : JpaRepository&amp;lt;File, Int&amp;gt;, FileRepositoryCustom

class FileRepositoryCustomImpl(
    private val query: JPAQueryFactory
) : QuerydslRepositorySupport(File::class.java), FileRepositoryCustom {
    override fun findWithLinkHistoryByNo(no: Int): List&amp;lt;File&amp;gt; {

        return query
            .select(file)
            .from(file)
            .leftJoin(fileLinkHistory).on(file.no.eq(fileLinkHistory.file.no))
            .fetch()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정렬&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;Member&amp;gt; result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(100))
                .orderBy(member.age.desc(), member.username.asc().nullsLast())
                .fetch();&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;desc() : 내림차순&lt;/li&gt;
&lt;li&gt;asc() : 오름차순&lt;/li&gt;
&lt;li&gt;nullsLast() : null을 제일 마지막으로&lt;/li&gt;
&lt;li&gt;nullsFirst() : null을 제일 처음으로&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이징&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;page&lt;/h4&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;public Page&amp;lt;MemberTeamDto&amp;gt; searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        // 검색 쿼리
        List&amp;lt;MemberTeamDto&amp;gt; content = queryFactory
            .select(new QMemberTeamDto(
                    member.id.as(&quot;memberId&quot;),
                    member.username,
                    member.age,
                    team.id.as(&quot;teamId&quot;),
                    team.name.as(&quot;teamName&quot;)))
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        // 카운트 쿼리 조립
        // 쿼리만 만들고 fetch같은것 사용하지 않음 
        JPAQuery&amp;lt;Member&amp;gt; countQuery = queryFactory
                .selectFrom(member)
                    .leftJoin(member.team, team)
                .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
                );
        return PageableExecutionUtils.getPage(content, pageable,()-&amp;gt;countQuery.fetchCount();
}&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;count 쿼리와 paging 쿼리를 별도로 작성해서 사용합니다.&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;slice&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slice 기법이란 일반적인 페이징 방식이 아닌 스크롤을 밑으로 내려가면서 데이터를 불러오는 방식입니다. Slice는 최종 페이지 수를 알 필요가 없으므로 count 쿼리가 필요 없습니다. JPA에서는 Page 대신 Slice로 반환하면 알아서 처리해주지만 QueryDSL에서는 직접 구현해야합니다.&lt;/p&gt;
&lt;p 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;N개의 데이터가 필요하다면 N+1 개의 데이터를 가져옵니다.&lt;/li&gt;
&lt;li&gt;결과 값의 개수 &amp;gt; N 이라면 다음 페이지가 존재한다는 뜻입니다.&lt;/li&gt;
&lt;li&gt;결과 값의 개수가 &amp;gt; N 라면 추가적으로 가져온 +1 데이터를 빼고 결과 리스트를 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RepositorySliceHelper&lt;/b&gt;&lt;br /&gt;Slice 관련 로직을 여러곳에서 사용하기 위해 클래스로 하나 만들어서 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class RepositorySliceHelper {

    public static &amp;lt;T&amp;gt; Slice&amp;lt;T&amp;gt; toSlice(List&amp;lt;T&amp;gt; contents, Pageable pageable) {

        boolean hasNext = isContentSizeGreaterThanPageSize(contents, pageable);
        return new SliceImpl&amp;lt;&amp;gt;(hasNext ? subListLastContent(contents, pageable) : contents, pageable, hasNext);
    }

    // 다음 페이지 있는지 확인
    private static &amp;lt;T&amp;gt; boolean isContentSizeGreaterThanPageSize(List&amp;lt;T&amp;gt; content, Pageable pageable) {
        return pageable.isPaged() &amp;amp;&amp;amp; content.size() &amp;gt; pageable.getPageSize();
    }

    // 데이터 1개 빼고 반환
    private static &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; subListLastContent(List&amp;lt;T&amp;gt; content, Pageable pageable) {
        return content.subList(0, pageable.getPageSize());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public Slice&amp;lt;NotificationDto&amp;gt; findNotificationByUsername(String username, Pageable pageable) {

        List&amp;lt;OrderSpecifier&amp;gt; ORDERS = getAllOrderSpecifiers(pageable);

        List&amp;lt;NotificationDto&amp;gt; results = query
                .select(new QNotificationDto(
                        notification.title,
                        notification.message,
                        notification.checked,
                        notification.notificationType,
                        notification.uuid,
                        notification.TeamId
                ))
                .from(notification)
                .where(notification.member.username.eq(username))
                .orderBy(ORDERS.stream().toArray(OrderSpecifier[]::new))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize() + 1)
                .fetch();

        return RepositorySliceHelper.toSlice(results, pageable);
    }&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;limit에서 +1로 데이터를 하나 더 가져오고 RepositorySliceHelper를 활용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;집계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL이 제공하는 모든 집함 함수를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
public void aggregation() throws Exception {
    List&amp;lt;Tuple&amp;gt; result = queryFactory
            .select(member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetch();

    Tuple tuple = result.get(0); 

    // get의 파라미터로 조회한 그대로를 넣으면 그에 대한 값이 나온다.
    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
}&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;Tuple은 Querydsl에서 제공하는 Tuple로 조회하는 것이 여러 개의 타입이 있을 때 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그룹&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;List&amp;lt;Tuple&amp;gt; result = queryFactory
                .select(team.name, member.age.avg())
                .from(member)
                .join(member.team, team) 
                // on으로 member.team_id = team.team_id 로 들어갑니다.
                .groupBy(team.name)
                .fetch();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그루핑은 join의 파라미터로 엔티티를 넣어주면 on절로 id값들을 묶어줍니다. .having도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 조인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt; List&amp;lt;Member&amp;gt; result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .on(member.name.eq(&quot;member&quot;))
                .fetch();

// 나가는 쿼리문
select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        team1_.name=?


Member findMember = queryFactory
        .selectFrom(member)
        .join(member.team, team).fetchJoin() // fetchJoin
        .where(member.username.eq(&quot;member1&quot;))
        .fetchOne();&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;join 파라미터의 id값끼리 on절로 묶입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;벌크 연산&lt;/h3&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;long count = queryFactory
            .update(member) // delete 도 가능
            // 수정할 필드, 수정
            .set(member.age, member.age.add(1))
            .execute(); // 다른 쿼리와 다르게 execute 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벌크연산은 영속성 컨텍스트를 무시하고 DB에 날리므로 항상 실행 이후에는 영속성 컨텍스트를 비워줘야 합니다. 스프링 데이터 JPA에서는 옵션으로 clearAutomatically을 사용하면 비워줄 수 있습니다. Querydsl에는 옵션이 없으므로 em.clear() 로 비워줘야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 개선&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;where 다중 파라미터 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where문의 경우 쉼표를 사용할 경우 null을 무시합니다. 예를 들면 where(null, member....) 이면 null은 무시되고 member...만 조건으로 들어갑니다.&amp;nbsp;이것을 활용하면 간단하고 재활용 가능한 코드로 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;@Test
void 동적쿼리() throws Exception{

private List&amp;lt;Member&amp;gt; searchMember2(String usernameCond, Integer ageCond) {
    return queryFactory
            .selectFrom(member)
            // where 파라미터를 함수로 구성
            .where(usernameEq(usernameCond),ageEq(ageCond))
}

private BooleanExpression usernameEq(String usernameCond) {
    return usernameCond != null ? member.username.eq(usernameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
    return ageCond != null ? member.age.eq(ageCond) : null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;exist 메서드 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 JPA에서 제공하는 exists는 조건에 해당하는 row 1개만 찾으면 바로 쿼리를 종료하기 때문에 전체를 찾아보지 않아 성능상 문제가 없습니다. 복잡하게 되면 메소드명으로만 쿼리를 표현하기 어렵기 때문에 보통 @Query를 사용하지만 JPQL의 경우 select의 exists를 지원하지 않습니다. 따라서 count쿼리를 사용해야 하는데 이는 총 몇 건인지 확인을 위해 전체를 봐야하기 때문에 성능이 나쁠 수 밖에 없습니다. 이를 개선하기 위해서 Querydsl의 selectOne과 fetchFirst(= limit 1)을 사용해서 직접 exists 쿼리를 구현해서 개선해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public Boolean exist(Long bookId){
    Integer fetchOne = queryFactory
        .selectOne()
        .from(book)
        .where(book.id.eq(bookId))
        .fetchFirst(); // 한건만 찾으면 바로 쿼리 종료(limit 1)

    return fetchOne != null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회결과가 없으면 null이 반환되기 때문에 null로 체크해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cross Join 회피&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public List&amp;lt;Customer&amp;gt; crossJoin(){
    return queryFactory
        .selectFrom(customer)
        .where(customer.customerNo.gt(customer.shop.shopNo))
        .fetch();
}

// 쿼리 결과
select
    ....
from
    customer customer_cross // cross 가 cross 조인을 의미함
join
    shop shop1_
where
    ....&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;where 문에서 customer.shop 코드로 인해 묵시적 join으로 Cross Join이 발생합니다. 일부의 DB는 이에 대해 어느정도 최적화가 지원되나 최적화 할수 있음에도 굳이 DB가 해주길 기다릴 필요는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;@Query(&quot;SELECT c FROM Customer c WHERE c.customerNo &amp;gt; c.shop.shopNo&quot;)
List&amp;lt;Customer&amp;gt; crossJoin();

// 쿼리 결과
select
    ....
from
    customer customer_cross // cross 가 cross 조인을 의미함
join
    shop shop1_
where
    ....&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;이는 Hibernate 이슈라서 Spring Data JPA도 동일하게 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;Customer&amp;gt; notCrossJoin(){
    return queryFactory
        .selectFrom(customer)
        .innerJoin(customer.shop, shop)
        .where(customer.customerNo.gt(shop.shopNo))
        .fetch();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 명시적으로 조인을 지정해줘서 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Group By 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 Group By를 실행하면 별도의 Order by이 쿼리에 포함되어 있지 않음에도 Filesort(정렬 작업이 쿼리 실행시 처리되는)가 필수적으로 발생합니다. 인덱스에 있는 컬럼들로 Group by를 한다면 이미 인덱스로 인해 컬럼들이 정렬된 상태이기 때문에 큰 문제가 되지 않으나 굳이 정렬이 필요 없는 Group by에서 정렬을 다시 할 필요는 없기 때문에 이 문제를 해결해야 하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;select 1
from ad_offset
group by customer_no
order by null asc;&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;MySQL에서는 order by null을 사용하면 Filesort가 제거되는 기능을 제공하지만 이는 QueryDSL에서는 지원되지 않습니다.&lt;br /&gt;따라서 이를 직접 구현해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.NullExpression;

public class OrderByNull extends OrderSpecifier {
    public static final OrderByNull DEFAULT = new OrderByNull();

    private OrderByNull() {
        super(Order.ASC, NullExpression.DEFAULT, NullHandling.Default);
    }
}

// 실제 사용
...
.groupBy(...)
.orderBy(OrderByNull.DEFAULT)
.fetch();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null을 그냥 넣게 되면 Querydsl의 정렬을 담당하는 OrderSpecifier 에서 제대로 처리하지 못합니다. Querydsl에서는 공식적으로 null에 대해 NullExpression.DEFAULT 클래스로 사용하길 권장하니 이를 활용합니다. &lt;b&gt;단, 페이징일 경우, order by null을 사용하지 못하므로 페이징이 아닌 경우에만 사용해야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 정렬이 필요하더라도, 조회 결과가 100건 이하라면, 애플리케이션에서 정렬해야합니다. 일반적인 자원의 입장에서 DB보다는 WAS의 자원이 더 저렴합니다. DB는 3~4대를 사용하더라도 WAS는 수십대를 유지하는 경우가 빈번합니다. 따라서 정렬이란 자원이 필요할 경우 WAS가 DB보다는 여유롭기 때문에 WAS에서 처리하는 것이 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;mysql 8.0 이전 버전에는 group by 가 사용된 쿼리는 그루핑되는 컬럼을 기준으로 묵시적인 정렬까지 함께 수행됐습니다. 그래서 group by 는 있지만 order by 절이 없는 쿼리에 대해서는 기본적으로 그루핑 컬럼에 대해서 정렬이 수행된 상태로 결과값을 반환했습니다. 하지만 8.0부터는 묵시적인 정렬은 실행되지 않고 반환됩니다. 이러한 이유때문에 8.0 버전 이전에는 group by 후 정렬이 필요하지 않은 경우 order by null 사용이 권장되었던 것입니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이징 개선&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;No Offset 으로 구조 변경하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 사용하는 페이징 쿼리는 일반적으로 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM items
WHERE 조건문
ORDER BY id DESC
OFFSET 페이지번호
LIMIT 페이지사이즈&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;앞에서 읽었던 행을 다시 읽어야 하기 때문&lt;/b&gt; 입니다. 예를 들어 offset이 10000이고 limit이 20이라면 결과적으로 10000개부터 20개를 읽어야하니 10020개를 읽고 10000개를 버리는 행위와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;No Offset 방식은 &lt;b&gt;조회 시작 부분을 인덱스로 빠르게 찾아 매번 첫 페이지만 읽도록 하는 방식&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;BookPaginationDto&amp;gt; paginationLegacy(String name, int pageNo, int pageSize) {
    return queryFactory
            .select(Projections.fields(BookPaginationDto.class,
                    book.id.as(&quot;bookId&quot;),
                    book.name,
                    book.bookNo))
            .from(book)
            .where(
                    book.name.like(name + &quot;%&quot;) // like는 뒤에 %가 있을때만 인덱스가 적용됩니다.
            )
            .orderBy(book.id.desc()) // 최신순으로
            .limit(pageSize) // 지정된 사이즈만큼
            .offset(pageNo * pageSize) // 지정된 페이지 위치에서 
            .fetch(); // 조회
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드는 위와 같이 offset + limit 까지 읽어와서 offset을 버리고 반환하는 형식 입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public List&amp;lt;BookPaginationDto&amp;gt; paginationNoOffset(Long bookId, String name, int pageSize) {

    return queryFactory
            .select(Projections.fields(BookPaginationDto.class,
                    book.id.as(&quot;bookId&quot;),
                    book.name,
                    book.bookNo))
            .from(book)
            .where(
                    ltBookId(bookId),
                    book.name.like(name + &quot;%&quot;)
            )
            .orderBy(book.id.desc())
            .limit(pageSize)
            .fetch();
}

private BooleanExpression ltBookId(Long bookId) {
    if (bookId == null) {
        return null; // BooleanExpression 자리에 null이 반환되면 조건문에서 자동으로 제거된다
    }

    return book.id.lt(bookId);
}&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;위 코드가 No Offset 방식으로 변경한 코드입니다. 함수에 들어오는 인자에 Id값이 존재합니다. 클라이언트 단에서 현재 갖고있는 id값의 마지막 값을 보내주면 id값을 조건에 넣고 limit으로 원하는 만큼 땡겨오는 방식입니다. 이렇게 작성하면 offset 만큼의 데이터를 읽을 필요가 없게 됩니다. 또한, 클러스터 인덱스인 Id값을 조건문으로 시작했기 때문에 빠르게 조회할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;커버링 인덱스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 충족시키는데 필요한 모든 컬럼을 갖고 있는 인덱스로 select / where / order by /group by 등에서 사용되는 모든 컬럼이 인덱스에 포함된 상태를 의미합니다. select 절에서 *를 이용하여 단순히 조회할 경우(where 조건문에 Non Clustered Key(보조 인덱스)를 사용한 경우) Non Clusterd Key에 있는 Clusted Key를 이용해 다시 실제 데이터 접근을 하여 데이터를 가져오게 됩니다.&lt;a href=&quot;https://jojoldu.tistory.com/476&quot;&gt;참고&lt;/a&gt; 결과적으로 1차적으로 보조 인덱스에 대해 검색하고 2차적으로 cluster index에 대해 검색하게 되는 것입니다. 커버링 인덱스를 사용할 경우 1차적인 검색만으로 끝낼 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Book 테이블을 만들고 예시를 들어보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create table book(
    id bigint not null auto_increment,
    book_no bigint not null,
    name varchar(255) not null,
    type varchar(255),
    primary key(id),
    key idx_name(name)
);

select id, book_no, book_type, name
from book
where name like '200%'
order by id desc
limit 10 offset 10000;&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;위의 select 문에서는 book_no와 book_type이 인덱스가 아니기 때문에 커버링 인덱스가 될 수 없습니다. 결국에는 pk값으로 2차적인 접근을 한다는 뜻인데, 그렇다면 pk값을 커버링 인덱스로 빠르게 가져오고 해당 pk값을 조건문으로 넣으면 pk값에 대한 조회로 빠르게 가져올 수 있을 것입니다. 따라서 Cluster Key(PK)를 커버링 인덱스로 빠르게 조회하고, 조회된 Key로 Select 컬럼들을 후속조회 하는 방식을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;BookPaginationDto&amp;gt; paginationCoveringIndex(String name, int pageNo, int pageSize) {
        // 1) 커버링 인덱스로 대상 조회
        List&amp;lt;Long&amp;gt; ids = queryFactory
                .select(book.id)
                .from(book)
                .where(book.name.like(name + &quot;%&quot;))
                .orderBy(book.id.desc())
                .limit(pageSize)
                .offset(pageNo * pageSize)
                .fetch();

        // 1-1) 대상이 없을 경우 추가 쿼리 수행 할 필요 없이 바로 반환
        if (CollectionUtils.isEmpty(ids)) {
            return new ArrayList&amp;lt;&amp;gt;();
        }

        // 2)
        return queryFactory
                .select(Projections.fields(BookPaginationDto.class,
                        book.id.as(&quot;bookId&quot;),
                        book.name,
                        book.bookNo,
                        book.bookType))
                .from(book)
                .where(book.id.in(ids))
                .orderBy(book.id.desc())
                .fetch(); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스 방식은 일반적인 페이징 방식에서는 대부분 적용할 수 있지만 몇 가지 단점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 많은 인덱스가 필요하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결국 쿼리의 모든 항목이 인덱스에 포함되어야하기 때문에 느린 쿼리가 발생할때마다 인덱스가 신규 생성될 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스 크기가 너무 커진다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스도 결국 데이터이기 때문에 너무 많은 항목이 들어가면 성능 상 이슈가 발생할 수 밖에 없는데, where절에 필요한 컬럼외에도 order by, group by, having 등에 들어가는 컬럼들까지 인덱스에 들어가게 되면 인덱스 크기가 너무 비대해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 양이 많아지고, 페이지 번호가 뒤로 갈수록 NoOffset에 비해 느리다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시작 지점을 PK로 지정하고 조회하는 NoOffset 방식에 비해서 성능 차이가 있음&lt;/li&gt;
&lt;li&gt;테이블 사이즈가 계속 커지면 No Offset 방식에 비해서는 성능 차이가 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <category>querydsl</category>
      <category>spring</category>
      <category>spring data jpa</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/47</guid>
      <comments>https://backtony.tistory.com/47#entry47comment</comments>
      <pubDate>Sat, 30 Dec 2023 18:57:59 +0900</pubDate>
    </item>
    <item>
      <title>Spring Data JPA - 성능 최적화</title>
      <link>https://backtony.tistory.com/46</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional(readOnly = true)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service layer에서 @Transactional을 대부분 사용합니다. 데이터 변경이 있는 경우 @Transactional을 붙이고 데이터 조회만 하는 경우라면 @Transactional(readOnly = true)를 명시합니다. readOnly가 true로 붙은 조회용의 경우 스냅샷이 생기지 않습니다. 즉, 영속성 컨텍스트에서 관리는 하고 있지만 dirty checking을 안하기 때문에 수정해도 DB에 결과가 반영되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;N+1&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 은 엔티티를 조회할 때, 해당 엔티티에 연관관계를 갖는 다른 엔티티를 프록시로 가져오면서(LazyLoading) 실제 데이터에 접근하는 시점에 해당 엔티티를 가져오기 위해 또 다른 조회 쿼리를 보내게 되는 현상을 말합니다. 즉, 엔티티를 조회하기 위한 find 메서드 하나를 호출 했더니 결과적으로 1개의 쿼리가 아닌 추가적인 N개의 쿼리가 더 발생한다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 문제는 fetch join / @EntityGraph / Batch Size 3가지 방식으로 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;fetch join &amp;amp; @EntityGraph&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member,Long&amp;gt; {
    @Query(&quot;select m from Member m join fetch m.team where m.name = :name&quot;)
    Optional&amp;lt;Member&amp;gt; findByName(String name);

    @EntityGraph(attributePaths = {&quot;team&quot;})
    List&amp;lt;Member&amp;gt; findByUsername(String username)
}&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;fetch join을 사용하면 entity의 fetch type를 LAZY로 지정했더라도 EAGER로 지정한 것으로 인식하여 한 번에 데이터를 가져옵니다.(fetch join이라고 했지만 jpql에서는 join fetch, left join fetch 이런 순서의 문법입니다.) entityGraph는 fetch join을 더 손쉽게 사용할 수 있는 방식입니다. @EntityGraph(attributePaths = {&quot;필드명&quot;}) 으로 페치 조인할 필드를 넣어주면 페치 조인해서 가져올 수 있습니다. 둘의 차이가 있다면 entityGraph는 left outer join이고, fetch join은 명시하지 않는 경우 inner join으로 수행됩니다.&lt;/p&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;Batch Size&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;batch size는 프록시로 가져온 연관관계 엔티티를 실제 접근하는 시점에 batchSize로 설정한 개수만큼 in 절을 사용해서 한번에 가져오는 방식입니다. fetch join 방식에 비해 몇 번의 쿼리가 더 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@BatchSize(size=100)
@OneToMany(mappedBy=&quot;team&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;batchSize는 application.yml에 전역적으로 설정해줄 수도 있고, 개별적으로 엔티티의 연관관계 매핑 애노테이션에 @BatchSize 애노테이션을 추가해 개별적으로 지정해줄 수도 있습니다. DB에 따라 IN절 파라미터를 1000으로 제한하기도 하므로 batchSize의 크기는 100 ~ 1000 사이의 값이 권장되고 애플리케이션 부하에 따라 판단해야 합니다. 애매하다면 100 ~ 500 사이의 값을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 fetch join이 더 좋은 방식인 것이 아닌가 할 수 있지만 fetch join에는 한계가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;fetch 조인 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;fetch 조인 대상에는 별칭을 줄 수 없습니다.&lt;/b&gt; = &lt;b&gt;따라서 페치 조인의 대상을 where문과 on절에서 사용할 수 없습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;fetch 조인은 나와 연관된 것들을 다 끌고 오겠다는 의미입니다.&lt;/b&gt; 대상에 별칭을 주고 그것을 활용해서 where문을 통해 몇개를 걸러서 가져오고 싶다고 한다면 fetch조인으로 접근하는게 아니라 따로 조회하는 것이 의도에 맞습니다.&lt;/li&gt;
&lt;li&gt;jpa에서 의도한 설계는 .을 통해 객체 그래프를 이어나간다면 모든 것에 접근할 수 있어야 한다는 것입니다. 그런데 fetch join에서 대상에 별칭을 주고 where로 데이터를 걸러서 가져온다면 특정 데이터에는 접근하지 못하게 됩니다. 객체 그래프라는 건 데이터를 다 조회한다는게 의도된 설계입니다. 따라서 절대로 fetch 조인 대상에는 별칭을 주면 안됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;따라서 페치 조인의 대상을 where문과 on절에서 사용할 수 없습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;둘 이상의 컬렉션은 fetch 조인 할 수 없습니다. -&amp;gt; &lt;b&gt;하나의 컬렉션과만 fetch join이 가능합니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XXToOne과 같이 단일 관계의 자식 테이블에는 Fetch Join 여러 번 사용해도 됩니다. 하지만 XXToMany의 경우 2개 이상의 OneToMany 자식 테이블에 Fetch Join을 시도했을때 MultipleBagFetchException이 발생합니다.&lt;/li&gt;
&lt;li&gt;이유는, fetch join을 둘 이상의 컬렉션과 하게 되면 곱하기 곱하기가 되어 데이터의 정합성이 맞지 않기 때문입니다.&lt;/li&gt;
&lt;li&gt;해결책은 Batch Size를 이용하는 것입니다. 자주 사용하는 컬렉션 쪽에는 Fetch join을 걸어주고, 나머지는 Lazy Loading으로 가져오면서 Batch Size를 활용하는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컬렉션을 fetch 조인하면 경우에 따라 페이징 API를 사용할 수 없습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일대일, 다대일 같은 단일 값 연관 필드는 fetch 조인해도 페이징 API가 사용 가능하지만. OneToMany의 경우에는 fetch join의 경우 페이징 API를 사용할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;oneToMany fetch join 페이징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ManyToXX, OneToOne 관계에서는 fetch join 시 페이징 이슈가 없습니다. 하지만 OneToMany 관계의 경우 fetch join 시 페이징 이슈가 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOobq3/btsCTvVKSx5/mubtixslhh6HKoXKmZ80qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOobq3/btsCTvVKSx5/mubtixslhh6HKoXKmZ80qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOobq3/btsCTvVKSx5/mubtixslhh6HKoXKmZ80qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOobq3%2FbtsCTvVKSx5%2Fmubtixslhh6HKoXKmZ80qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;766&quot; height=&quot;621&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oneToMany의 경우 fetch join을 하게 되면 데이터가 뻥튀기가 됩니다. team 테이블에 팀A는 한개의 row만 있지만 fetch join을 하게 되면서 결과는 team은 2개의 row로 증가합니다. 중복을 제거하기 위해 distinct를 추가해도 데이터가 완전히 똑같지 않으므로 distinct만으로는 팀A 행을 한개의 row로 만들 수 없습니다. 그러나 JPA에서 distinct를 사용하는 경우 애플리케이션 단에서 pk를 기준으로 중복 제거를 시도합니다. 여기서 이슈가 되는 부분은 페이징입니다. DB에서는 distinct로 데이터가 걸러지지 않았고 애플리케이션에서 중복이 제거되기 때문에 DB는 데이터가 뻥튀기된 상태로 페이징을 처리하기 때문에 원하는대로 동작하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface TeamRepository extends JpaRepository&amp;lt;Team,Long&amp;gt; {
    @Query(&quot;select distinct t from Team t join fetch t.member where t.name = :name&quot;)
    Optional&amp;lt;Member&amp;gt; findByName(String name, Pageable page);
}&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;따라서 oneToMany의 fetch join의 결과값은 페이징을 사용할 수 없습니다. 또한 페이징이 아니더라도 데이터를 사용하기 위해서는 distinct를 명시해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hibernate 6.0 버전부터는 default로 distinct가 추가되므로 distinct를 명시하지 않아도 됩니다. &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.0/migration-guide/migration-guide.html#query-sqm-distinct&quot;&gt;https://docs.jboss.org/hibernate/orm/6.0/migration-guide/migration-guide.html#query-sqm-distinct&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Projection &amp;amp; DTO&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 데이터를 가져올 때, 엔티티 전체 컬럼이 아닌 특정 컬럼만 가져오고 싶을 경우 사용합니다. fetch join으로 전체 컬럼이 아닌 일부의 컬럼만 가져올 때 유용할 수 있습니다. 하지만 실무에서는 대부분 사용하지 않습니다. 특정 로직에만 맞는 로직이 생성되기 때문에 재사용성이 떨어질 뿐더러 애초에 조인으로 엄청난 컬럼이 생겨날 정도로 도메인을 설계하지 않습니다. 그리고 몇 개의 컬럼만 가져온다고 해서 네트워크 성능이 비약적으로 증가하지도 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Page 방식에서 totalPages 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Page방식에서는 전체 데이터의 개수를 구해주는데 이 데이터의 개수를 구하는 카운트 쿼리는 프로젝션만 count(id값)으로 변경되고 from 이후에는 기존 쿼리랑 동일합니다. 만약 기존 쿼리가 조인으로 인해 복잡해지면 카운트 쿼리도 조인되어 복잡한 쿼리가 나갑니다. 하지만 카운트 쿼리는 그냥 개수만 세면 되므로 join할 필요가 없는 경우도 있습니다. 이런 경우 카운트 쿼리를 분리해서 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member,Long&amp;gt; {
    @Query(value = &quot;select m from Member m left join m.team t&quot;,
        countQuery = &quot;select count(m) from Member m&quot;)
    Page&amp;lt;Member&amp;gt; findByAge(int age, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;벌크성 수정 쿼리&lt;/h2&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member,Long&amp;gt; {
    @Modifying(clearAutomatically = true)
    @Query(&quot;update Member m set m.age = m.age + 1 where m.age &amp;gt;= :age&quot;)
    int bulkAgePlus(@Param(&quot;age&quot;) int age);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Modifying : 조회성 쿼리가 아니라는 것을 알려주는 것으로 JPA가 만들어 줄때 .getResultList가 아니라 .executeUpdate를 붙여주게 됩니다.&lt;/li&gt;
&lt;li&gt;벌크성 수정 쿼리는 영향 받은 데이터의 개수가 반환됩니다.&lt;/li&gt;
&lt;li&gt;clearAutomatically = true : 쿼리 날린 후 영속선 컨텍스트 비워줍니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벌크성 수정 쿼리는 영속성 컨텍스트를 무시하고 바로 DB에 날리기 때문에 영속성 컨텍스트와 DB 간의 불일치가 생깁니다. 따라서 벌크성 쿼리 이후에는 영속성 컨텍스트를 비워줘야하는데 이 옵션이 비워주는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database</category>
      <category>N+1</category>
      <category>spring</category>
      <category>spring data jpa</category>
      <category>성능</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/46</guid>
      <comments>https://backtony.tistory.com/46#entry46comment</comments>
      <pubDate>Sat, 30 Dec 2023 17:36:56 +0900</pubDate>
    </item>
    <item>
      <title>JPA Batch Insert와 JDBC Batch Insert</title>
      <link>https://backtony.tistory.com/45</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bulk Insert&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL을 사용하면서 Batch Insert를 수행하기 위해서는 2가지 방법이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA Batch Insert + Table 전략&lt;/li&gt;
&lt;li&gt;JDBC Batch Insert&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IDENTITY 전략으로 Batch INSERT가 불가능한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA + Batch Insert를 MySQL에서 사용하기 위해서는 ID 전략을 Table 전략으로 수정해야 합니다. 하지만 MySQL은 Sequence 전략이 없습니다. 일반적으로 MySQL에서 사용하는 IDENTITY 전략은 auto_increment으로 PK 값을 자동으로 증분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Insert를 실행하기 전까지는 ID에 할당된 값을 알 수 없기 때문에 Transactional Write Behind를 할 수 없고 결과적으로 Batch Insert를 진행할 수 없습니다. 간단하게 말하자면, Entity를 persist 하려면 @Id로 지정한 필드에 값이 필요한데 IDENTITY(auto_increment) 타입은 실제 DB에 insert를 해야만 값을 얻을 수 있기 때문에 batch 처리가 불가능한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Batch Insert 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 Bulk Insert를 사용하기 위해서 JPA Batch를 사용하나 JDBC Native Query를 사용하나 다음 설정이 application.yml에 반드시 필요합니다.&lt;/p&gt;
&lt;pre class=&quot;go&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;spring:
    datasource:
        url: jdbc:mysql://localhost:3306/db명?rewriteBatchedStatements=true
    jpa:
        properties:
            hibernate:
                ## bulk insert 옵션 ##
                # 정렬 옵션
                order_inserts: true
                order_updates: true
                # 한번에 나가는 배치 개수 -&amp;gt; 100개의 insert를 1개로 보낸다.
                jdbc:
                    batch_size: 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rewriteBatchedStatements를 true로 세팅해두지 않으면 Insert쿼리가 여전히 각각 나가게 됩니다. order_inserts, order_updates 옵션은 insert하는 것과 update 하는 것의 순서를 말합니다. 예를들어 트랜잭션에 부모 엔티티를 save한 후 자식 엔티티를 save하는 순서가 있다고 가정해보면 원래는 다음과 같이 쿼리가 나갑니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;insert into 부모 value a1
insert into 자식 value b1
insert into 부모 value a2
insert into 자식 value b2&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;이렇게 정렬이 되지 않고 쿼리가 나가게 되면 쿼리를 묶어서 한번에 사용할 수 없습니다. 옵션들을 true로 주게되면 아래와 같이 쿼리가 바뀌어 나갑니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;insert into 부모 value (a1, a2)
insert into 자식 value (b1, b2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch Insert가 정확하게 나가는지 확인하고 싶으시면 다음과 같이 옵션을 추가하면 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
    datasource:
        url: jdbc:mysql://localhost:3306/db명?rewriteBatchedStatements=true&amp;amp;profileSQL=true&amp;amp;logger=Slf4JLogger&amp;amp;maxQuerySizeToLog=999999&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;postfileSQL = true : Driver에 전송하는 쿼리를 출력합니다.&lt;/li&gt;
&lt;li&gt;logger=Slf4JLogger : Driver에서 쿼리 출력시 사용할 로거를 설정합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 드라이버 : 기본값은 System.err 로 출력하도록 설정되어 있기 때문에 필수로 지정해 줘야 합니다.&lt;/li&gt;
&lt;li&gt;MariaDB 드라이버 : Slf4j 를 이용하여 로그를 출력하기 때문에 설정할 필요가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;maxQuerySizeToLog=999999 : 출력할 쿼리 길이
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 드라이버 : 기본값이 0 으로 지정되어 있어 값을 설정하지 않을경우 아래처럼 쿼리가 출력되지 않습니다.&lt;/li&gt;
&lt;li&gt;MariaDB 드라이버 : 기본값이 1024 로 지정되어 있습니다. MySQL 드라이버와는 달리 0으로 지정시 쿼리의 글자 제한이 무제한으로 설정됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 application.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  h2:
    console:
      enabled: true

  datasource:
    url: jdbc:mariadb://localhost:3307/test?rewriteBatchedStatements=true&amp;amp;profileSQL=true&amp;amp;logger=Slf4JLogger&amp;amp;maxQuerySizeToLog=999999
    driver-class-name: org.mariadb.jdbc.Driver
    username: root
    password: root


  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
        default_batch_fetch_size: 100
        ## bulk insert 옵션 ##
        # 정렬 옵션
        order_inserts: true
        order_updates: true
        # 배치 개수 옵션
        jdbc.batch_size: 100

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;table 전략과 sequence 전략을 다 확인해보기 위해 mariadb를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키 맵핑 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Batch Insert를 사용하기 위해서는 앞서 언급했듯이 Identity 전략을 사용할 수 없습니다. 따라서 Sequence 또는 Table 전략을 사용해야 합니다. Sequence 전략은 오라클, PostgreSQL, H2 데이터베이스에서 사용할 수 있지만 MySQL에서는 사용할 수 없어서 MySQL에서는 Table 전략을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sequence 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트입니다. 시퀀스 사용 코드는 IDENTITY 전략과 같지만 내부 동작 방식이 다릅니다. 시퀀스 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회해서 가져오고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장합니다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Board {

    @Id
    @SequenceGenerator(
            name = &quot;BOARD_SEQ_GENERATOR&quot;,
            sequenceName = &quot;BOARD_SEQ&quot;,
            initialValue = 1, allocationSize = 50
    )
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = &quot;BOARD_SEQ_GENERATOR&quot;)
    private Long id;

    private String title;
}&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;@SequenceGenerator를 사용해서 시퀀스 생성기를 등록합니다. name은 식별자 생성기 이름을 정해주고 이 시퀀스 생성자를 사용할 곳에 세팅해주면 됩니다. (GenerateValue에 generator로 세팅해주면 됩니다.) 데이터베이스에는 sequenceName으로 설정한 &quot;BOARD_SEQ&quot;가 데이터베이스의 BOARD_SEQ 시퀀스와 매핑됩니다. yml 옵션에서 ddl을 생성하게 해두었다면 board 테이블이 생성되기 전에 다음과 같은 쿼리가 나갑니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 위 Board에서 설정한 initialValue와 allocationSize는 디폴트 값을 명시적으로 세팅해준거라 지워도 됩니다.
create sequence board_seq start with 1 increment by 50

create sequence [sequenceName]
start with [initialValue] increment by [allocationSize]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cf) @SequenceGenerator 속성 정리&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;속성&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;기본값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;식별자 생성기 이름&lt;/td&gt;
&lt;td&gt;필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sequenceName&lt;/td&gt;
&lt;td&gt;데이터베이스에 등록되어 있는 시퀀스 이름&lt;/td&gt;
&lt;td&gt;hibernate_sequence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;initialValue&lt;/td&gt;
&lt;td&gt;DDL 생성 시에만 사용되며 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allocationSize&lt;/td&gt;
&lt;td&gt;시퀀스 한 번 호출에 증가하는 수(성능 최적화)&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;catalog, schema&lt;/td&gt;
&lt;td&gt;데이터베이스 catalog, schema 이름&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;allocationSize는 기본값이 50이므로 시퀀스를 호출할 때마다 값이 50씩 증가합니다. 기본값이 50인 이유는 최적화 때문입니다. 시퀀스 전략은 데이터베이스와 2번 통신합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;식별자를 구하기 위해 데이터 베이스 시퀀스 조회
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT BOARD_SEQ.NEXTVAL FROM DUAL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;INSERT INTO BOARD ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 allocationSize를 사용합니다. 간단히 설명하자면, 여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당합니다. 예를 들어 allocationSize 값이 50이면 DB에서 시퀀스를 한 번에 50 증가시킨 다음에 1~50까지는&amp;nbsp;메모리에서&amp;nbsp;식별자를&amp;nbsp;할당하는데&amp;nbsp;사용합니다.&amp;nbsp;그리고&amp;nbsp;51이&amp;nbsp;되면&amp;nbsp;다시&amp;nbsp;시퀀스&amp;nbsp;값을&amp;nbsp;DB에서&amp;nbsp;100으로&amp;nbsp;증가시키고&amp;nbsp;51~100까지 메모리에서 식별자를 할당합니다. 이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bach insert가 정상적으로 나가는지 확인해보기 위해 100개를 저장시켜 보았습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;/board&quot;)
public void test(){
    List&amp;lt;Board&amp;gt; ls = new ArrayList&amp;lt;&amp;gt;();

    for (int i=0; i&amp;lt;100;i++){
        Board title = Board.builder().title(&quot;title&quot;).build();
        ls.add(title);
    }
    boardRepository.saveAll(ls);
}
------------------------------------------------------------------------
Query: insert into board (title, id) values (?, ?), parameters ['title',1],['title',2],['title',3],['title',4],['title',5],['title',6],['title',7],['title',8],['title',9],['title',10],['title',11],['title',12],['title',13],['title',14],['title',15],['title',16],['title',17],['title',18],['title',19],['title',20],['title',21],['title',22],['title',23],['title',24],['title',25],['title',26],['title',27],['title',28],['title',29],['title',30],['title',31],['title',32],['title',33],['title',34],['title',35],['title',36],['title',37],['title',38],['title',39],['title',40],['title',41],['title',42],['title',43],['title',44],['title',45],['title',46],['title',47],['title',48],['title',49],['title',50],['title',51],['title',52],['title',53],['title',54],['title',55],['title',56],['title',57],['title',58],['title',59],['title',60],['title',61],['title',62],['title',63],['title',64],['title',65],['title',66],['title',67],['title',68],['title',69],['title',70],['title',71],['title',72],['title',73],['title',74],['title',75],['title',76],['title',77],['title',78],['title',79],['title',80],['title',81],['title',82],['title',83],['title',84],['title',85],['title',86],['title',87],['title',88],['title',89],['title',90],['title',91],['title',92],['title',93],['title',94],['title',95],['title',96],['title',97],['title',98],['title',99],['title',100]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;한번에 처리되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Table 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스 전략을 흉내내는 전략입니다. 먼저 키 생성 테이블을 만들어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;create table 테이블명 (
    sequence_name varchar(255) not null,
    next_val bigint,
    primary key (sequence_name)
)

-- 예시
create table table_sequence (
    TABLE_SEQ varchar(255) not null,
    next_val bigint,
    primary key (TABLE_SEQ)
)&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;sequence_name 컬럼은 시퀀스 이름으로 사용하고 next_val 컬럼을 시퀀스 값으로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Tb {

    @Id
    @TableGenerator(
            name = &quot;TABLE_SEQ_GENERATOR&quot;,
            table = &quot;TABLE_SEQUENCE&quot;, // 위에서 생성한 테이블명
            pkColumnName = &quot;TABLE_SEQ&quot; // 위에서 생성한 테이블에 sequence_name
    )
    @GeneratedValue(strategy = GenerationType.TABLE,
                    generator = &quot;TABLE_SEQ_GENERATOR&quot;)
    private long id;

    private String title;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 방식과 매우 유사합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@TableGenerator 속성 정리&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;속성&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;기본값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;식별자 생성기 이름&lt;/td&gt;
&lt;td&gt;필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;table&lt;/td&gt;
&lt;td&gt;키생성 테이블명&lt;/td&gt;
&lt;td&gt;hibernate_sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pkColumnName&lt;/td&gt;
&lt;td&gt;시퀀스 컬럼명&lt;/td&gt;
&lt;td&gt;sequence_name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valueColumnName&lt;/td&gt;
&lt;td&gt;시퀀스 값 컬럼명&lt;/td&gt;
&lt;td&gt;next_val&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pkColumnValue&lt;/td&gt;
&lt;td&gt;키로 사용할 값 이름&lt;/td&gt;
&lt;td&gt;엔티티 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;initialValue&lt;/td&gt;
&lt;td&gt;초기값, 마지막으로 생성된 값이 기준(즉, 세팅한 다음 값부터 할당&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allocationSize&lt;/td&gt;
&lt;td&gt;시퀀스 한 번 호출에 증가하는 수&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;catalog, schema&lt;/td&gt;
&lt;td&gt;데이터베이스 catalog, schema 이름&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uniqueConstraints(DDL)&lt;/td&gt;
&lt;td&gt;유니크 제약 조건 지정&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 안에 값이 없으면 JPA가 값을 INSERT하면서 초기화하므로 값을 미리 넣어둘 필요는 없습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bach insert가 정상적으로 나가는지 확인해보기 위해 100개를 저장시켜 보았습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class Controller {

    private final TbRepository tbRepository;

    @PostMapping(&quot;/tb&quot;)
    public void testTb(){
        List&amp;lt;Tb&amp;gt; ls = new ArrayList&amp;lt;&amp;gt;();

        for (int i=0; i&amp;lt;100;i++){
            Tb title = Tb.builder().title(&quot;title&quot;).build();
            ls.add(title);
        }
        tbRepository.saveAll(ls);
    }
}
----------------------------------------------------------------------------------------

[QUERY] insert into tb (title, id) values ('title', 1),('title', 2),('title', 3),('title', 4),('title', 5),('title', 6),('title', 7),('title', 8),('title', 9),('title', 10),('title', 11),('title', 12),('title', 13),('title', 14),('title', 15),('title', 16),('title', 17),('title', 18),('title', 19),('title', 20),('title', 21),('title', 22),('title', 23),('title', 24),('title', 25),('title', 26),('title', 27),('title', 28),('title', 29),('title', 30),('title', 31),('title', 32),('title', 33),('title', 34),('title', 35),('title', 36),('title', 37),('title', 38),('title', 39),('title', 40),('title', 41),('title', 42),('title', 43),('title', 44),('title', 45),('title', 46),('title', 47),('title', 48),('title', 49),('title', 50),('title', 51),('title', 52),('title', 53),('title', 54),('title', 55),('title', 56),('title', 57),('title', 58),('title', 59),('title', 60),('title', 61),('title', 62),('title', 63),('title', 64),('title', 65),('title', 66),('title', 67),('title', 68),('title', 69),('title', 70),('title', 71),('title', 72),('title', 73),('title', 74),('title', 75),('title', 76),('title', 77),('title', 78),('title', 79),('title', 80),('title', 81),('title', 82),('title', 83),('title', 84),('title', 85),('title', 86),('title', 87),('title', 88),('title', 89),('title', 90),('title', 91),('title', 92),('title', 93),('title', 94),('title', 95),('title', 96),('title', 97),('title', 98),('title', 99),('title', 100) [Created on: Fri Feb 11 11:11:20 KST 2022, duration: 2, connection-id: 70, statement-id: 0, resultset-id: 0,    at com.zaxxer.hikari.pool.ProxyStatement.executeBatch(ProxyStatement.java:127)]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC Batch Insert&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jdbc Template을 사용하면 MySQL의 IDENTITY 전략을 사용하더라도 아래와 같은 코드로 배치 INSERT 쿼리를 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class MemberJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public void insertMemberList(List&amp;lt;Member&amp;gt; memberList){
        jdbcTemplate.batchUpdate(&quot;insert into member (name) values (?)&quot;,
                new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, memberList.get(i).getName());
                    }

                    @Override
                    public int getBatchSize() {
                        return memberList.size();
                    }
                });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;/member&quot;)
public void testMember(){
    List&amp;lt;Member&amp;gt; ls = new ArrayList&amp;lt;&amp;gt;();

    for (int i=0; i&amp;lt;100;i++){
        Member member = Member.builder().name(&quot;name&quot;).build();
        ls.add(member);
    }
    memberJdbcRepository.insertMemberList(ls);
}

--------------------------------------------------------------------------------------------
Query: insert into member (name) values (?), parameters ['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name'],['name']&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 사용 시 bulk insert를 하게 되면 그만큼의 데이터가 영속화 된 이후 flush하게 됩니다. 메모리가 충분하다면 문제가 없겠지만 너무 많은 데이터를 영속화 시키게 된다면 out of memory가 발생하게 됩니다. 따라서 적절한 개수만큼 나눠서 영속화 시키고 flush, clear을 반복해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jdbcTemplate을 사용한다면 영속성 컨텍스트를 사용하지 않으니 상관없겠지라고 생각할 수 있지만 MySQL을 예시로 들면 MySQL Client가 Server로 전달하는 Packet의 크기는 제한되어 있습니다. 따라서 너무 많은 양의 데이터를 한 번에 넣게 되면 Packet의 크기가 허용치를 넘어 예외가 발생하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW VARIABLES LIKE 'max_allowed_packet&amp;rsquo;;&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;이는 max_allowed_packet 필드로 확인할 수 있습니다. 즉, jdbcTemplate을 사용하더라도 JPA와 마찬가지로 적절한 크기로 나눠서 insert를 해야 합니다.&lt;/p&gt;
&lt;p data-ke-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>Database</category>
      <category>batch insert</category>
      <category>JDBC</category>
      <category>JPA</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/45</guid>
      <comments>https://backtony.tistory.com/45#entry45comment</comments>
      <pubDate>Sat, 30 Dec 2023 16:16:26 +0900</pubDate>
    </item>
    <item>
      <title>JPA - LOCK</title>
      <link>https://backtony.tistory.com/44</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;애그리거트와 트랜잭션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 주문 애그리거트에 대해 운영자는 배송 상태로 변경할 때 사용자는 배송지 주소를 변경하면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq9kaL/btsCOHiAANB/NqTQxUevH0BpnntItKeloK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq9kaL/btsCOHiAANB/NqTQxUevH0BpnntItKeloK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq9kaL/btsCOHiAANB/NqTQxUevH0BpnntItKeloK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq9kaL%2FbtsCOHiAANB%2FNqTQxUevH0BpnntItKeloK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;415&quot; height=&quot;407&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 운영자와 고객이 동시에 한 주문 애그리거트를 수정하는 과정을 보여줍니다. 트랜잭션마다 리포지토리는 새로운 애그리거트 객체를 생성하므로 운영자 스레드가 고객 스레드는 같은 주문 애그리거트를 나타내는 다른 객체를 구하게 됩니다. 운영자 스레드가 고객 스레드는 개념적으로 동일한 애그리거트지만 물리적으로 서로 다른 애그리거트 객체를 사용합니다. 때문에 운영자 스레드가 주문 애그리거트 객체를 배송 상태로 변경하더라도 고객 스레드가 사용하는 주문 애그리거트 객체에는 영향을 주지 않습니다. 고객 스레드 입장에서 주문 애그리거트 객체는 아직 배송 상태 전이므로 배송지 정보를 변경할 수 있습니다. 이 상황에서 두 스레드는 각각 트랜잭션을 커밋할 때 수정한 내용을 DB에 반영합니다. 이 시점에 배송 상태로 바뀌고 배송지 정보도 바뀝니다. 이 순서의 문제점은 운영자는 기존 배송지 정보를 이용해서 배송 상태로 변경했는데 그 사이 고객은 배송지 정보를 변경했다는 점입니다. 즉, 애그리거트의 일관성이 깨집니다. 이를 방지하기 위해서는 다음 두 가지 중 하나를 해야 합니다.&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;운영자가 배송지 정보를 조회하고 상태를 변경하는 동안, 고객이 애그리거트를 수정하지 못하게 막는다.&lt;/li&gt;
&lt;li&gt;운영자가 배송지 정보를 조회한 이후 고객이 정보를 변경하면, 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지는 애그리거트 자체의 트랜잭션과 관련이 있습니다. DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요합니다. 애그리거트에 대해 사용할 수 있는 대표적인 트랜잭션 처리 방식에는 선점(Pessimistic) 잠금과 비선점(Optimistic) 잠금의 두 가지 방식이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPA 락 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA가 제공하는 락 옵션과 용도를 알아봅시다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style16&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;락 모드&lt;/th&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;낙관적 락&lt;/td&gt;
&lt;td&gt;OPTIMISTIC&lt;/td&gt;
&lt;td&gt;낙관적 락을 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낙관적 락&lt;/td&gt;
&lt;td&gt;OPTIMISTIC_FORCE_INCREMENT&lt;/td&gt;
&lt;td&gt;낙관적 락 + 버전정보를 강제로 증가한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비관적 락&lt;/td&gt;
&lt;td&gt;PESSIMISTIC_READ&lt;/td&gt;
&lt;td&gt;비관적 락, 읽기 락을 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비관적 락&lt;/td&gt;
&lt;td&gt;PESSIMISTIC_WRITE&lt;/td&gt;
&lt;td&gt;비관적 락, 쓰기 락을 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비관적 락&lt;/td&gt;
&lt;td&gt;PESSIMISTIC_FORCE_INCREMENT&lt;/td&gt;
&lt;td&gt;비관적 락 + 버전 정보를 강제로 증가한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;NONE&lt;/td&gt;
&lt;td&gt;락을 걸지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;READ&lt;/td&gt;
&lt;td&gt;JPA 1.0 호환 기능으로 OPTIMISTIC과 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;WRITE&lt;/td&gt;
&lt;td&gt;JPA 1.0 호환 기능으로 OPTIMISTIC_FORCE_INCREMENT와 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;낙관적 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA가 제공하는 낙관적 락은 @Version을 사용합니다. 따라서 낙관적 락을 사용하려면 버전이 있어야합니다. 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 특징이 있습니다. 낙관적 락에서 발생하는 예외는 다음과 같습니다.&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;OptimisticLockException(JPA 예외)&lt;/li&gt;
&lt;li&gt;StaleObjectStateException(하이버네이트 예외)&lt;/li&gt;
&lt;li&gt;ObjectOptimisticLockingFailureException(스프링 예외 추상화)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;NONE&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 옵션을 적용하지 않아도 엔티티에 @Version이 적용된 필드만 있으면 낙관적 락이 적용됩니다.&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;동작: 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다.(UPDATE 쿼리 사용) 이때 데이터베이스의 버전 값이 현재 버전이 아니면 예외가 발생한다.&lt;/li&gt;
&lt;li&gt;이점: 두 번의 갱신 분실 문제를 예방한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OPTIMISTIC&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크합니다. 즉, 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장합니다.&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;용도: 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 한다. 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장한다.&lt;/li&gt;
&lt;li&gt;동작: 트랜잭션을 커밋할 때 버전 정보를 조회해서(SELECT 쿼리) 현재 엔티티의 버전과 같은지 검증한다. 만약 같지 않으면 예외가 발생한다.&lt;/li&gt;
&lt;li&gt;이점: OPTIMISTIC 옵션은 DIRTY READ와 NONE-REPEATABLE READ를 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OPTIMISTIC_FORCE_INCREMENT&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락을 사용하면서 버전 정보를 강제로 증가시킵니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;용도: 논리적인 단위의 엔티티 묶음을 관리할 수 있다. 예를 들어 게시물과 첨부파일이 일대다, 다대일의 양방향 연관관계이고 첨부파일이 연관관계의 주인이다. 게시물을 수정하는 데 단순히 첨부파일만 추가하면 게시물의 버전은 증가하지 않는다. 해당 게시물은 물리적으로는 변경되지 않았지만, 논리적으로는 변경되었다. 이때 게시물의 버전도 강제로 증가하려면 이 옵션을 사용한다.&lt;/li&gt;
&lt;li&gt;동작: 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킨다. 이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외가 발생한다. 추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생한다. 따라서 총 2번의 버전 증가가 나타날 수 있다.&lt;/li&gt;
&lt;li&gt;이점: 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길게 풀어썼지만, 애그리거트 루트를 수정하지 않았지만 애그리거트 루트가 관리하는 엔티티를 수정했을 때 애그리거트 루트도 버전을 강제로 증가시킨다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비관적 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방식입니다. 주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않습니다. 비관적 락은 주로 PESSIMISTIC_WRITE 모드를 사용합니다. 비관적 락은 다음과 같은 특징이 있습니다.&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;엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락에서 발생하는 예외는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PessimisticLockException(JPA예외)&lt;/li&gt;
&lt;li&gt;PessimisticLockingFailureException(스프링 예외 추상화)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PESSIMICTIC_WRITE&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락이라면 일반적으로 이 옵션을 말합니다. 데이터베이스 쓰기 락을 걸 때 사용합니다.&lt;/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;용도: 데이터베이스에 쓰기 락을 건다.&lt;/li&gt;
&lt;li&gt;동작: 데이터베이스 select for update를 사용해서 락을 건다.&lt;/li&gt;
&lt;li&gt;이점: NON-REPEATABLE READ를 방지한다. 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PESSIMICTIC_READ&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸때 사용합니다. 일반적으로 잘 사용하지 않습니다. 데이터베이스 대부분은 방언에 의해 PESSIMISTIC_WRITE로 동작합니다.&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;MySQL: lock in share mode&lt;/li&gt;
&lt;li&gt;PostgreSQL: for share&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PESSIMISTIC_FORCE_INCREMENT&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락중 유일하게 버전 정보를 사용합니다. 비관적 락이지만 버전 정보를 강제로 증가시킵니다. 하이버네이트는 nowait를 지원하는 데이터베이스에 대해서 for update nowait옵션을 적용합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오라클: for update nowait&lt;/li&gt;
&lt;li&gt;postgreSQL: for update nowait&lt;/li&gt;
&lt;li&gt;nowait을 지원하지 않으면 for update가 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;타임 아웃&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기하게 되는데 무한정 기다릴 수 없으므로 타임아웃 시간을 줄 수 있습니다. Spring DATA JPA에서는 다음과 같이 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface UserRepository extends JpaRepository&amp;lt;User,Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({@QueryHint(name = &quot;javax.persistence.lock.timeout&quot;, value =&quot;10000&quot;)})
    Optional&amp;lt;User&amp;gt; findForUpdateById(Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선점 잠금&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9QgJA/btsCMoX5Rv6/IOjNxMygspknuoRb2vcfIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9QgJA/btsCMoX5Rv6/IOjNxMygspknuoRb2vcfIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9QgJA/btsCMoX5Rv6/IOjNxMygspknuoRb2vcfIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9QgJA%2FbtsCMoX5Rv6%2FIOjNxMygspknuoRb2vcfIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;878&quot; height=&quot;1166&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영자 스레드가 선점 잠금 방식으로 애그리거트를 구한 뒤 이어서 고객 스레드가 같은 애그리거트를 구하고 있습니다. 이때 고객 스레드는 운영자 스레드가 애그리거트에 대한 잠금을 해제할 때까지 블로킹됩니다. 운영자 스레드가 애그리거트를 수정하고 트랜잭션을 커밋하면 잠금이 해제되고 이 순간 대기하고 있던 고객 스레드가 애그리거트에 접근할 수 있게 됩니다. 운영자 스레드가 트랜잭션을 커밋한 뒤에 고객 스레드가 애그리거트를 구하게 되므로 고객 스레드는 운영자 스레드가 수정한 애그리거트의 내용을 보게 됩니다. 이를 통해 한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선점 잠금은 보통 DBMS가 제공하는 행단위 잠금을 사용해서 구현합니다. 오라클을 비롯한 다수의 DBMS가 for update와 같은 쿼리를 사용해서 특정 레코드에 한 커넥션만 접근할 수 있는 잠금장치를 제공합니다. JPA 프로바이더와 DBMS에 따라 잠금 모드 구현이 다릅니다. 하이버네이트의 경우 PESSIMISTIC_WRITE를 잠금 모드로 사용하면 for update 쿼리를 이용해서 선점 잠금을 구현합니다. Spring Data JPA는 @LOCK 애너테이션을 사용해서 잠금 모드를 지정합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface UserRepository extends JpaRepository&amp;lt;User,Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional&amp;lt;User&amp;gt; findForUpdateById(Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;교착상태&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태가 발생하지 않도록 주의해야 합니다. 예를 들어, 다음과 같은 순서로 두 스레드가 잠금을 시도한다고 해봅시다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스레드1: A 애그리거트에 대한 선점 잠금 구함&lt;/li&gt;
&lt;li&gt;스레드2: B 애그리거트에 대한 선점 잠금 구함&lt;/li&gt;
&lt;li&gt;스레드1: B 애그리거트에 대한 선점 잠금 시도&lt;/li&gt;
&lt;li&gt;스레드2: A 애그리거트에 대한 선점 잠금 시도&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서에 따르면 스레드1은 영원히 B 애그리거트에 대한 선점 잠금을 구할 수 없게 되면서 교착 상태에 빠지게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선점 잠금에 따른 교착 상태는 상대적으로 사용자 수가 많을 때 발생할 가능성이 높고, 사용자 수가 많아지면 교착 상태에 빠지는 스레드가 많아지고 시스템이 죽게 됩니다. 이 문제가 발생하지 않도록 하려면 잠금을 구할 때 최대 대기 시간을 지정해야 합니다. Spring Data JPA에서 선점 잠금을 시도할 때 최대 대기 기간을 지정하려면 힌트를 사용해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface UserRepository extends JpaRepository&amp;lt;User,Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({@QueryHint(name = &quot;javax.persistence.lock.timeout&quot;, value =&quot;10000&quot;)})
    Optional&amp;lt;User&amp;gt; findForUpdateById(Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비선점 잠금&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선점 잠금이 강력해 보이긴 하지만 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결되는 것은 아닙니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;575&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RrVO8/btsCP5pUj5o/4Wi6KUGjrnx9EbdRmtYtk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RrVO8/btsCP5pUj5o/4Wi6KUGjrnx9EbdRmtYtk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RrVO8/btsCP5pUj5o/4Wi6KUGjrnx9EbdRmtYtk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRrVO8%2FbtsCP5pUj5o%2F4Wi6KUGjrnx9EbdRmtYtk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;431&quot; height=&quot;575&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;575&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;운영자는 배송을 위해 주문 정보를 조회한다. 시스템은 정보를 제공한다.&lt;/li&gt;
&lt;li&gt;고객이 배송지 변경을 위해 변경 폼을 요청한다. 시스템은 변경 폼을 제공한다.&lt;/li&gt;
&lt;li&gt;고객이 새로운 배송지를 입력하고 폼을 전송해서 배송지를 변경한다.&lt;/li&gt;
&lt;li&gt;운영자가 1번에서 조회한 주문 정보를 기준으로 배송지를 정하고 배송 상태 변경을 요청한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제는 운영자가 배송지 정보를 조회하고 배송 상태로 변경하는 사이에 고객이 배송지를 변경한다는 것입니다. 운영자는 고객이 변경하기 전 배송지 정보를 이용하여 배송 준비를 한 뒤에 배송 상태를 변경하게 됩니다. 즉, 배송 상태 변경 전에 배송지를 한 번 더 확인하지 않으면 엉뚱한 곳으로 물건을 보내게 됩니다. 이 문제는 선점 잠금 방식으로는 해결할 수 없습니다. 이때 필요한 것이 비선점 잠금입니다. 비선점 잠금은 동시에 접근하는 것을 막는 대신 &lt;b&gt;변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식&lt;/b&gt; 입니다. 비선점 잠금을 구현하려면 애그리거트 버전으로 사용할 숫자 타입 프로퍼티를 추가해야 합니다. 애그리거트를 수정할 때마다 버전으로 사용할 프로퍼티 값이 1씩 증가하는데 이때 다음과 같은 쿼리가 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE aggtable SET version = version + 1, colx = ?, coly = ?
WHERE aggid = ? and version = 현재 버젼&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 수정할 애그리거트와 매핑되는 테이블의 버전 값이 현재 애그리거트의 버전과 동일한 경우에만 데이터를 수정합니다. 그리고 수정에 성공하면 버전 값을 1 증가시킵니다. 다른 트랜잭션이 먼저 데이터를 수정해서 버전 값이 바뀌면 데이터 수정에 실패하게 됩니다. 이를 그림으로 표현하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vzOdb/btsCTxTy8jp/CTXdGIngwqSVNSuZeCP2Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vzOdb/btsCTxTy8jp/CTXdGIngwqSVNSuZeCP2Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vzOdb/btsCTxTy8jp/CTXdGIngwqSVNSuZeCP2Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvzOdb%2FbtsCTxTy8jp%2FCTXdGIngwqSVNSuZeCP2Z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;407&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;간단한 비선점 잠금 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 예시 상황을 코드로 보기 전에 우선 가장 간단하게 비선점 잠금을 사용하는 법을 살펴봅시다. JPA는 버전을 이용한 비선점 잠금 기능을 지원합니다. 버전으로 사용할 필드에 @Version 애너테이션을 붙이고 매핑되는 테이블에 버전을 저장할 컬럼을 추가하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version
    private long version;

    private String name;

    public Team(String name) {
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Version으로 추가한 버전 관리 필드는 JPA가 직접 관리하므로 개발자가 임의로 수정하면 안 됩니다.(벌크 연산 제외)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비선점 잠금을 위한 쿼리를 실행할 때 쿼리 실행 경과로 수정된 행의 개수가 0이면 이미 누군가 앞서 데이터를 수정한 것입니다. 이는 트랜잭션이 충돌한 것이므로 트랜잭션 종료 시점에 예외가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TeamService {

    private final TeamRepository teamRepository;

    @Transactional
    public void sleepAndChangeStatusPLAY(Long teamId){
        Team team = teamRepository.findById(teamId).orElseThrow(() -&amp;gt; new RuntimeException(&quot;team not found&quot;));
        try {
            Thread.sleep(10_000);
            team.play();
        } catch (Exception e){
        }
    }

    @Transactional
    public void changeStatusCANCEL(Long teamId){
        Team team = teamRepository.findById(teamId).orElseThrow(() -&amp;gt; new RuntimeException(&quot;team not found&quot;));
        team.cancel();
    }
}&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;다음과 같은 시나리오로 동작하면 트랜잭션이 충돌하면서 OptimisticLockingFailureException이 발생합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;sleepAndChangeStatusPLAY 가 호출되면 version값이 1인 team 엔티티를 가져와서 10초 동안 쉽니다.&lt;/li&gt;
&lt;li&gt;10초 동안 쉬는 사이에 changeStatusCANCEL 메서드에서 같은 team 엔티티(version 값이 1인)를 가져와서 상태를 업데이트하면서 version을 2로 업데이트합니다.&lt;/li&gt;
&lt;li&gt;sleepAndChangeStatusPLAY 메서드에서 10초가 지나고 team 엔티의 상태를 업데이트하고 version을 2로 올리면서 커밋합니다.&lt;/li&gt;
&lt;li&gt;version이 이미 2이기 때문에 충돌이 발생하면서 OptimisticLockingFailureException 예외가 발생합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리에 아무런 옵션도 주지 않았지만 @Version 컬럼이 엔티티에 있기 때문에 낙관적인 락으로 동작합니다. 아무런 옵션도 주지 않아 NONE 옵션으로 동작하여 조회시에는 version이 업데이트 되지 않고 아무런 수정이 없어도 업데이트 되지 않습니다. 트랜잭션이 종료되면서 충돌 여부를 확인할 수 있기 때문에 presentation 계층에서 예외를 처리해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
public class TeamRestController {

    private final TeamService teamService;

    @GetMapping(&quot;sleep/{teamId}&quot;)
    public void sleep(@PathVariable Long teamId){
        try {
            teamService.sleepAndChangeStatusPLAY(teamId);
        } catch (OptimisticLockingFailureException ex){
            log.error(&quot;{} : {}&quot;,&quot;낙관적락 예외 발생&quot;, &quot;동작 하는 과정에서 누가 중간에 수정했다.&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 상황 비선점 잠금 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 간단한 비선점 사용 방법을 알아봤으니 앞서 설명한 예시 상황을 비선점 잠금으로 해결해 봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;575&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/may0G/btsCP6bjcPU/Vkev2kZIZPyNNXpDDoiwT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/may0G/btsCP6bjcPU/Vkev2kZIZPyNNXpDDoiwT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/may0G/btsCP6bjcPU/Vkev2kZIZPyNNXpDDoiwT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmay0G%2FbtsCP6bjcPU%2FVkev2kZIZPyNNXpDDoiwT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;431&quot; height=&quot;575&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;575&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황을 해결하기 위해서는 운영자가 주문 정보를 조회하는 시점에 응답값으로 version 값을 같이 반환해야 합니다. 그리고 4번의 배송 상태 변경 요청 시에 조회 시점에 응답값으로 받았던 version 컬럼도 함께 요청 값으로 보내줍니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {

    private OrderRepository orderRepository;

    @Transactional
    public void startShipping(StartShippingRequestDto req) {
        Order order = orderRepository.findById(req.getOrderId())
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;order not found&quot;));
        if (order.matchVersion(req.getVersion())) { // 요청으로 받은 version값과 DB의 version 값 비교
            throw new VersionConflictException(); // 불일치 하는 경우 예외 발생
        }
        order.startShipping();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
public class OrderRestController {

    private final OrderService orderService;

    @PostMapping
    public void start(@RequestBody StartShippingRequest startShippingRequest){
        try {
            orderService.startShipping(startShippingRequest.toStartShippingRequestDto());
        } catch (OptimisticLockingFailureException | VersionConflictException e){
            log.error(&quot;{}&quot;,&quot;낙관적락 예외 발생&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VersionConflictException는 이미 누군가 애그리거트를 수정했다는 것을 의미하고, OptimisticLockingFailureException은 누군가 거의 동시에 애그리거트를 수정했다는 것을 의미합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;강제 버전 증가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애그리거트에 애그리거트 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경된다고 합시다. 이 경우 JPA는 루트 엔티티의 버전 값을 증가시키지 않습니다. 연관된 엔티티의 값이 변경된다고 해도 루트 엔티티 자체의 값을 바뀌는 것이 없기 때문입니다. 이런 JPA 특징은 애그리거트 관점에서 보면 문제가 됩니다. 비록 루트 엔티티의 값이 바뀌지 않았더라도 애그리거트의 구성 요소 중 일부 값이 바뀌면 논리적으로 그 애그리거트는 바뀐 것입니다. 따라서 애그리거트 내에 어떤 구성요소의 상태가 바뀌면 루트 애그리거트의 버전값이 증가해야 비선점 잠금이 올바르게 동작한다고 할 수 있습니다. JPA는 이런 문제를 처리할 수 있도록 엔티티를 구할 때 강제로 버전 값을 증가시키는 잠금 모드를 지원합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface OrderRepository extends JpaRepository&amp;lt;Order,Long&amp;gt; {

    @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
    Optional&amp;lt;Order&amp;gt; findOptimisticLockModeById(Long orderId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리를 사용할 경우 엔티티의 상태가 변경되었는지에 관계없이 트랜잭션 종료 시점에 버전 값이 증가됩니다. 또한, 루트 엔티티가 아닌 다른 엔티티나 밸류가 변경되더라도 버전 값이 증가되므로 비선점 잠금 기능을 안전하게 적용할 수 있습니다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>JPA</category>
      <category>lock</category>
      <category>spring</category>
      <author>backtony</author>
      <guid isPermaLink="true">https://backtony.tistory.com/44</guid>
      <comments>https://backtony.tistory.com/44#entry44comment</comments>
      <pubDate>Sat, 30 Dec 2023 15:59:34 +0900</pubDate>
    </item>
  </channel>
</rss>