<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DevHJ</title>
    <link>https://devhj.tistory.com/</link>
    <description>개발관련 정리</description>
    <language>ko</language>
    <pubDate>Sat, 1 Aug 2026 00:31:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>하르싼</managingEditor>
    <item>
      <title>Spring Batch + Quartz 통합 배치 스케줄러: QuartzBatchControl 프로젝트 소개</title>
      <link>https://devhj.tistory.com/75</link>
      <description>&lt;h1&gt;QuartzBatchControl&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch와 Quartz Scheduler를 통합 관리하는 웹 기반 배치 작업 관리 시스템&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QuartzBatchControl&lt;/b&gt;&lt;span&gt;은 &lt;/span&gt;&lt;b&gt;Spring Batch&lt;/b&gt;&lt;span&gt;와 &lt;/span&gt;&lt;b&gt;Quartz Scheduler&lt;/b&gt;&lt;span&gt;를 통합하여,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영자가 스케줄 기반 혹은 수동으로 배치잡을 관리해야 하는 상황에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;웹 UI&lt;/b&gt;&lt;/span&gt;로 편리하게 작업을 수행하고 실행 이력을 확인할 수 있도록 설계된&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실시간 통합 배치 관리 시스템&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 효율성을 높이고, 로그와 상태 추적을 구조화함으로써&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 운영의 안정성과 가시성을 제공합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⚠️ 본 프로젝트는 코드 제공을 목적으로 하며,&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실제 운영 환경에서는 각 조직의 CI/CD 환경, 다중 서버 구조,&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;**로그 저장 방식(S3 등)**에 맞게 구성 및 확장하여 사용해야 합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  프론트엔드는 &lt;span&gt;&lt;b&gt;Cursor AI&lt;/b&gt;&lt;/span&gt;를 통해 개발되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  전체 코드 보기: &lt;a href=&quot;https://github.com/devHjlee/QuartzBatchControl&quot;&gt;https://github.com/devHjlee/QuartzBatchControl&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Release&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.0.0 (최신) : 배치 프로젝트(quartzbatchcontrol-batch)는 메인 백엔드와 완전히 분리되어 외부 JAR 실행 구조로 변경되었습니다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;아키텍처 분리&lt;/b&gt;: 기존 백엔드(Spring Boot) 프로젝트에서 스프링 배치 코드를 완전히 분리하여, 별도의 &lt;code&gt;quartzbatchcontrol-batch&lt;/code&gt; 프로젝트로 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 배치 실행&lt;/b&gt;: 백엔드에서는 커맨드라인을 통해 외부 배치 JAR을 실행하는 구조로 변경&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 JAR 관리&lt;/b&gt;: 배치 프로젝트는 jar로 빌드하여 특정 경로에 위치시키고, 백엔드에서 설정파일의 경로를 변경 (설정된 디렉토리 변경 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 외부 배치 프로젝트 빌드
# GitHub Actions 기준: JAR 빌드 및 EC2 배포, 최신 심볼릭 링크 갱신
- name: Build batch jar
  run: |
    cd quartzbatchcontrol-batch
    ./gradlew clean bootJar

- name: Copy jar to EC2
  uses: appleboy/scp-action@v0.1.4
  with:
    host: ${{ secrets.EC2_HOST }}
    username: ec2-user
    key: ${{ secrets.EC2_SSH_KEY }}
    source: &quot;quartzbatchcontrol-batch/build/libs/*.jar&quot;
    target: &quot;/home/ec2-user/externaljob&quot;

- name: Update latest symlink
  uses: appleboy/ssh-action@v0.1.10
  with:
    script: |
      cd /home/ec2-user/externaljob
      ln -sf $(ls -t *.jar | head -n 1) latest&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;b&gt;로그 관리 강화&lt;/b&gt;: 배치 프로젝트에 로그 관리를 위한 리스너(BatchJobExecutionListener)가 기본 설정되어 있어, 배치 실행 시 로그가 자동으로 관리됨 (다중서버일 경우 외부 저장소 필요)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;: 새로운 배치 잡은 &lt;code&gt;quartzbatchcontrol-batch&lt;/code&gt; 프로젝트에 생성하고 BatchJobExecutionListener 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.0.0&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;내장형 배치&lt;/b&gt;: 쿼츠배치 백엔드(Spring Boot) 프로젝트 내에서 가벼운 스프링 배치 잡을 직접 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직접 실행&lt;/b&gt;: 백엔드 프로젝트에 등록된 배치 잡을 직접 수행하는 구조&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 프로젝트&lt;/b&gt;: 배치와 스케줄러가 한 프로젝트 내에 통합되어 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;b&gt;대시보드&lt;/b&gt;: 배치/Quartz 작업 현황 및 성공/실패 통계 시각화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 관리&lt;/b&gt;: 배치 작업 등록, 수정, 즉시 실행, 파라미터 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 로그&lt;/b&gt;: 실행 이력, 파라미터, 상태, 상세 로그 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Quartz 관리&lt;/b&gt;: Quartz 작업 등록, 수정, 스케줄 관리, 상태 변경(일시정지/재개/삭제)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Quartz 로그&lt;/b&gt;: Quartz 작업 실행 이력 및 상세 로그 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검색/필터&lt;/b&gt;: 다양한 조건으로 작업 및 로그 검색&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 모니터링&lt;/b&gt;: 작업 상태 및 결과를 실시간으로 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;JDK 17 이상&lt;/li&gt;
&lt;li&gt;Node.js 16 이상&lt;/li&gt;
&lt;li&gt;MySQL 8.0 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 및 실행&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Backend 서버를 먼저 실행&lt;/li&gt;
&lt;li&gt;Frontend 실행 (&lt;a href=&quot;http://localhost:Port&quot;&gt;http://localhost:Port&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;외부 배치 JAR은 별도 실행 필요 시 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Batch (2.0.0 Release)&lt;/h4&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;git clone [repository-url]
cd quartzBatchControl-batch
./gradlew build
java -jar quartzbatchcontrol-batch-0.0.1-SNAPSHOT.jar --spring.profiles.active=sync-only  (등록된 배치잡 목록을 DB 등록)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Backend&lt;/h4&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone [repository-url]
cd QuartzBatchControl-Backend
./gradlew build
./gradlew bootRun&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Frontend&lt;/h4&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;cd quartzbatchcontrol-frontend
npm install
npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ow9ZT/btsOxyVUpeQ/PY58dehIkgKsraVvQtTaK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ow9ZT/btsOxyVUpeQ/PY58dehIkgKsraVvQtTaK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ow9ZT/btsOxyVUpeQ/PY58dehIkgKsraVvQtTaK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fow9ZT%2FbtsOxyVUpeQ%2FPY58dehIkgKsraVvQtTaK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;808&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배치 관리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2310&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bByzmT/btsOxN6mvhi/HL1qOkoJBIyZylOjXfJjg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bByzmT/btsOxN6mvhi/HL1qOkoJBIyZylOjXfJjg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bByzmT/btsOxN6mvhi/HL1qOkoJBIyZylOjXfJjg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbByzmT%2FbtsOxN6mvhi%2FHL1qOkoJBIyZylOjXfJjg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;392&quot; data-origin-width=&quot;2310&quot; data-origin-height=&quot;392&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;873&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BKQjH/btsOx6xNbzB/Q7ZqhJLKPoxRmkcSRdqrvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BKQjH/btsOx6xNbzB/Q7ZqhJLKPoxRmkcSRdqrvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BKQjH/btsOx6xNbzB/Q7ZqhJLKPoxRmkcSRdqrvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBKQjH%2FbtsOx6xNbzB%2FQ7ZqhJLKPoxRmkcSRdqrvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;873&quot; height=&quot;715&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;715&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배치 로그&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2322&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sz72W/btsOyne3cwr/wRC12t6fKk66YmOeFzEChK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sz72W/btsOyne3cwr/wRC12t6fKk66YmOeFzEChK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sz72W/btsOyne3cwr/wRC12t6fKk66YmOeFzEChK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsz72W%2FbtsOyne3cwr%2FwRC12t6fKk66YmOeFzEChK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2322&quot; height=&quot;450&quot; data-origin-width=&quot;2322&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;배치 상세 로그 (2.0.0 Release)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baJga5/btsOzN4AlSD/IPuBy4VA1PNWXkTAn81yCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baJga5/btsOzN4AlSD/IPuBy4VA1PNWXkTAn81yCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baJga5/btsOzN4AlSD/IPuBy4VA1PNWXkTAn81yCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaJga5%2FbtsOzN4AlSD%2FIPuBy4VA1PNWXkTAn81yCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1682&quot; height=&quot;837&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Quartz 관리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtAlOK/btsOyFNjAwN/xeTfxUW7YxLEJXaWiFYctk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtAlOK/btsOyFNjAwN/xeTfxUW7YxLEJXaWiFYctk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtAlOK/btsOyFNjAwN/xeTfxUW7YxLEJXaWiFYctk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtAlOK%2FbtsOyFNjAwN%2FxeTfxUW7YxLEJXaWiFYctk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2308&quot; height=&quot;415&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;415&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;741&quot; data-origin-height=&quot;671&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ea84OW/btsOzePcUMF/uvtYQF4ghO6qeClOPRVMtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ea84OW/btsOzePcUMF/uvtYQF4ghO6qeClOPRVMtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ea84OW/btsOzePcUMF/uvtYQF4ghO6qeClOPRVMtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fea84OW%2FbtsOzePcUMF%2FuvtYQF4ghO6qeClOPRVMtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;741&quot; height=&quot;671&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Quartz 로그&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2316&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chLqCk/btsOzEGOjGY/tKHc0rfP1IMnyXhDnlRTe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chLqCk/btsOzEGOjGY/tKHc0rfP1IMnyXhDnlRTe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chLqCk/btsOzEGOjGY/tKHc0rfP1IMnyXhDnlRTe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchLqCk%2FbtsOzEGOjGY%2FtKHc0rfP1IMnyXhDnlRTe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2316&quot; height=&quot;655&quot; data-origin-width=&quot;2316&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;b&gt;Backend&lt;/b&gt;: Spring Boot, Spring Batch, Quartz Scheduler, Spring Security, Spring Data JPA, QueryDSL, MySQL&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Frontend&lt;/b&gt;: Vue.js 3, Bootstrap Admin 2, Axios, Pinia, Vite&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기타&lt;/b&gt;: Swagger(OpenAPI), ESLint, Prettier&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;폴더 구조&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;QuartzBatchControl/
├── batch-logs/                   # quartzBatchControl-batch 로그폴더
├── externaljob                   # quartzBatchControl-batch Jar 폴더
│   ├── latest                    # 최신 quartzBatchControl-batch.jar 의 심볼릭링크
├── QuartzBatchControl-Backend/   # Spring Boot 백엔드
├── quartzBatchControl-batch/     # Batch 백엔드 (2.0.0 Release)
├── quartzbatchcontrol-frontend/  # Vue.js 프론트엔드
│   ├── src/
│   │   ├── components/
│   │   ├── api/
│   │   └── ...
│   ├── public/
│   └── ...
└── docs/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3005&quot; data-origin-height=&quot;3840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb3bUD/btsOx9OPvIA/KNtJFfKM24QeTF82nOd8Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb3bUD/btsOx9OPvIA/KNtJFfKM24QeTF82nOd8Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb3bUD/btsOx9OPvIA/KNtJFfKM24QeTF82nOd8Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb3bUD%2FbtsOx9OPvIA%2FKNtJFfKM24QeTF82nOd8Ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3005&quot; height=&quot;3840&quot; data-origin-width=&quot;3005&quot; data-origin-height=&quot;3840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;erDiagram
    BATCH_JOB_META ||--o{ QUARTZ_JOB_META : &quot;has&quot;
    BATCH_JOB_META ||--o{ BATCH_JOB_LOG : &quot;generates&quot;
    QUARTZ_JOB_META ||--o{ QUARTZ_JOB_LOG : &quot;generates&quot;
    QRTZ_JOB_DETAILS ||--|| QRTZ_TRIGGERS : &quot;has&quot;
    QUARTZ_JOB_META ||--|| QRTZ_JOB_DETAILS : &quot;maps to&quot;
    BATCH_JOB_CATALOG }o--|| BATCH_JOB_META : &quot;maps by job_name (logical)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;통합 작업 관리 플로우&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A6dHL/btsOyGSYHRd/nDKwl4sHqUxT7uqpTJcPv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A6dHL/btsOyGSYHRd/nDKwl4sHqUxT7uqpTJcPv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A6dHL/btsOyGSYHRd/nDKwl4sHqUxT7uqpTJcPv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA6dHL%2FbtsOyGSYHRd%2FnDKwl4sHqUxT7uqpTJcPv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3840&quot; height=&quot;578&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    A[시작] --&amp;gt; B{작업 타입 선택}

    %% 배치 작업 플로우
    B --&amp;gt;|배치| C{배치 작업 존재?}
    C --&amp;gt;|No| D[배치 작업 생성]
    C --&amp;gt;|Yes| E[배치 작업 메타 등록]
    D --&amp;gt; E
    E --&amp;gt; F[작업 파라미터 설정]
    F --&amp;gt; G{즉시 실행?}
    G --&amp;gt;|Yes| H[배치 작업 실행]
    H --&amp;gt; J[실행 로그 기록]
    J --&amp;gt; K[종료]

    %% Quartz 작업 플로우
    B --&amp;gt;|Quartz| L{Quartz 작업 타입}
    L --&amp;gt;|SIMPLE| M[Quartz 작업 클래스 확인]
    L --&amp;gt;|BATCH| N[배치 작업 선택]
    M --&amp;gt; O[작업 메타 등록]
    N --&amp;gt; O
    O --&amp;gt; P[크론 표현식 설정]
    P --&amp;gt; Q[Quartz 작업 등록]
    Q --&amp;gt; R[트리거 생성]
    R --&amp;gt; S[스케줄러 시작]
    S --&amp;gt; K

    %% 모니터링 플로우
    K --&amp;gt; T[대시보드 모니터링]
    T --&amp;gt; U{실패 발생?}
    U --&amp;gt;|Yes| V[로그 상세 확인]
    U --&amp;gt;|No| W[정상 종료]
    V --&amp;gt; W&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 관계&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;BatchJobMeta (배치 작업 메타데이터)&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;QuartzJobMeta와 1:N 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QuartzJobMeta (Quartz 작업 메타데이터)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Quartz 작업의 기본 정보 관리&lt;/li&gt;
&lt;li&gt;BatchJobMeta와 N:1 관계&lt;/li&gt;
&lt;li&gt;작업 타입 (SIMPLE/BATCH) 구분&lt;/li&gt;
&lt;li&gt;QRTZ_JOB_DETAILS와 1:1 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BatchJobLog (배치 작업 로그)&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;BatchJobMeta와 N:1 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QuartzJobLog (Quartz 작업 로그)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Quartz 작업 실행 이력&lt;/li&gt;
&lt;li&gt;QuartzJobMeta와 N:1 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QRTZ_JOB_DETAILS (Quartz 작업 상세)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Quartz 작업 상세 정보&lt;/li&gt;
&lt;li&gt;QRTZ_TRIGGERS와 1:1 관계&lt;/li&gt;
&lt;li&gt;QuartzJobMeta와 1:1 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API 문서&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger(OpenAPI) 제공&lt;br /&gt;백엔드 서버 실행 후 &lt;a href=&quot;http://localhost:8080/swagger-ui.html&quot;&gt;http://localhost:8080/swagger-ui.html&lt;/a&gt; 접속&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 엔드포인트 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/api/dashboard&lt;/code&gt; : 대시보드 통계&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/batch&lt;/code&gt; : 배치 작업 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/batch-log&lt;/code&gt; : 배치 로그&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/quartz-jobs&lt;/code&gt; : Quartz 작업 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/quartz-log&lt;/code&gt; : Quartz 로그&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/auth&lt;/code&gt; : 인증/회원가입&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>Spring</category>
      <category>cursor ai</category>
      <category>quartz spring batch</category>
      <category>quartz 실무 적용</category>
      <category>Spring Batch</category>
      <category>배치 관리 ui</category>
      <category>배치 모니터링</category>
      <category>배치 스케줄러</category>
      <category>스케줄링 시스템</category>
      <category>잡 스케줄링</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/75</guid>
      <comments>https://devhj.tistory.com/75#entry75comment</comments>
      <pubDate>Sat, 31 May 2025 17:06:09 +0900</pubDate>
    </item>
    <item>
      <title>Cursor AI로 게시판 풀스택 개발 실험 후기</title>
      <link>https://devhj.tistory.com/74</link>
      <description>&lt;p&gt;
이번 프로젝트는 &lt;strong&gt;Cursor AI&lt;/strong&gt;의 코드 생성 능력을 검증해보고, 실제 실무 수준의 게시판 시스템을 얼마나 빠르게 구현할 수 있는지를 확인하기 위한 실험이었습니다.&lt;br&gt;
백엔드는 제가 익숙한 Spring Boot 기반으로, 프론트는 유지보수만 경험해봤던 Vue 3 기반으로 구성했습니다.
&lt;/p&gt;

&lt;h3&gt;프로젝트 개요&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;목적:&lt;/strong&gt; Cursor AI를 통해 바이브 코딩 실험 - 얼마나 빠르게 정확한 기능을 구현할 수 있는가?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;백엔드:&lt;/strong&gt; Spring Boot + Spring Security + JPA + MySQL&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;프론트엔드:&lt;/strong&gt; Vue 3 + TypeScript + Vite + Tailwind CSS + Pinia + Axios&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href=&quot;https://github.com/devHjlee/cursorboard&quot; target=&quot;_blank&quot;&gt;https://github.com/devHjlee/cursorboard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;사용한 Cursor 프롬프트&lt;/h3&gt;

&lt;h4&gt;백엔드 프롬프트&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Spring Boot 기반의 백엔드 게시판 API를 JPA + MySQL 환경으로 구현해주세요...
Spring Boot 기반의 백엔드 게시판 API를 JPA + MySQL 환경으로 구현해주세요.

패키지 구조:
- 루트 도메인은 user, post, config, security이며,
- user, post 패키지에는 각각 다음과 같은 하위 계층이 존재합니다:
  - api: Controller
  - application: Service
  - domain: Entity
  - infrastructure: Repository

공통 개발 요구:
- 모든 컨트롤러는 @RestController로 작성해주세요.
- 요청/응답은 반드시 DTO로 구성해주세요.
- 공통 예외 처리를 위한 GlobalExceptionHandler를 구현해주세요.
- 모든 엔티티에는 createdAt, updatedAt 필드를 포함해주세요. (JPA Auditing 기반)

기능 요구 사항:

1. 사용자 인증/인가:
   - Spring Security 기반의 JWT 인증/인가 처리 적용
   - 로그인, 회원가입 API는 인증 없이 접근 가능해야 함
   - 회원가입 시 이메일 중복 체크 API 제공
   - 회원가입은 이메일, 비밀번호만 입력
   - 사용자 권한은 USER / ADMIN 두 가지

2. 게시글 관련 기능:
   - 게시글 작성, 수정, 삭제 기능
     - 로그인 사용자만 가능하며, 삭제는 작성자 또는 ADMIN 권한만 가능
   - 게시글 목록 조회: 인증 없이 접근 가능
   - 게시글 상세 조회:
     - 제목, 내용, 댓글 목록까지 포함된 단일 API 제공
     - 댓글에는 작성자 이메일, 댓글 내용 포함

3. 댓글 기능:
   - 게시글 상세 하단에 댓글 목록 출력
   - 로그인 사용자는 댓글 등록 가능
   - 삭제는 본인 또는 ADMIN만 가능

기술 스택:
- Spring Boot 3.x
- Spring Security + JWT
- JPA + MySQL
- DTO 분리 설계
- 단일 API 호출로 게시글 + 댓글 동시 반환 (상세 조회)&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;프론트엔드 프롬프트&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Vue 3 + Vite 기반의 TypeScript SPA 프론트엔드 프로젝트를 생성해주세요...
기본 조건:
- 전체 SPA 구조이며, Vue Router를 사용해 각 화면을 라우팅 처리해주세요.
- 상태 관리는 Pinia를 사용하고, 로그인 상태 및 JWT 토큰을 Pinia로 관리해주세요.
- Axios를 통해 백엔드와 통신하며, JWT 토큰은 Authorization 헤더에 자동 포함되도록 설정해주세요.
- Axios 설정은 별도 파일로 분리해주세요.
- 프로젝트 구조는 views/, components/, store/, router/ 디렉토리로 구성해주세요.
- 기본적인 Tailwind CSS 또는 기타 UI 스타일을 적용해주세요. 화면이 너무 밋밋하지 않게 구성해주세요.

화면 구성:
1. 로그인 페이지 (/login)
   - Email, Password 입력 필드
   - 로그인 요청 후 JWT 토큰을 localStorage에 저장
   - 로그인 성공 시 게시글 목록 페이지(/posts)로 이동

2. 게시글 목록 페이지 (/posts)
   - 전체 게시글 조회
   - 게시글 클릭 시 상세 페이지(/posts/:id)로 이동
   - 각 게시글에는 수정(/posts/:id/edit) 및 삭제 버튼 표시
   - 상단에는 게시글 등록 버튼(/posts/write) 표시

3. 게시글 상세 페이지 (/posts/:id)
   - 게시글 제목, 내용, 작성자 출력
   - 하단에 댓글 목록 출력
   - 댓글 작성 기능 포함

4. 게시글 등록 페이지 (/posts/write)
   - 제목, 내용 입력 필드
   - 저장 시 목록 페이지로 이동

5. 게시글 수정 페이지 (/posts/:id/edit)
   - 기존 게시글 내용 로드 후 수정 가능
   - 수정 완료 시 상세 페이지로 이동

6. Not Found 페이지 (/404)
   - 존재하지 않는 경로로 접근 시 표시

공통 요구사항:
- 로그인하지 않은 사용자가 보호된 라우트에 접근하면 /login으로 리디렉션 처리
- 모든 인증이 필요한 버튼(글쓰기, 수정, 삭제 등)은 로그인 상태에서만 노출
- 댓글 작성 시 로그인 상태가 아니라면 로그인 페이지로 안내
- API 요청 실패 시 사용자에게 알림 메시지를 띄우는 기본 에러 핸들링 처리

기타:
- 작동이 가능한 실제 코드로 구성해주세요.
- 컴포넌트와 페이지는 명확하게 분리하고, 파일명과 폴더명은 일관성 있게 작성해주세요.&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;작업 및 검증 시간&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;백엔드: Cursor가 생성한 구조를 제가 직접 검토하고, 빠진 예외 처리, 실무에서 필요한 보완 작업 및 오류 해결에 약 2시간 소요&lt;/li&gt;
  &lt;li&gt;프론트엔드: Vue 구조나 문법은 깊게 검증하지 못했지만, 주요 기능 (로그인, 글 CRUD, 댓글 등) 테스트 기준 1시간 이내로 완료&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;프로젝트 구조 요약 (Backend + Frontend)&lt;/h3&gt;

&lt;h4&gt;Backend 주요 구조&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;com.cursorboard
├── config              # CORS, Security 설정 등
├── security            # JWT 토큰 발급 및 필터 처리
├── user                # 회원가입, 로그인 API (api/application/domain/infrastructure)
├── post                # 게시글, 댓글 API (api/application/domain/infrastructure)&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Frontend 주요 구조&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;src/
├── views/              # login, posts, postDetail, write 등 페이지
├── components/         # Header, PostItem 등 UI 컴포넌트
├── store/              # auth 상태 관리 (JWT 저장)
├── router/             # Vue Router 설정
├── api/axios.ts        # Axios + JWT 헤더 처리
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;결과 요약&lt;/h3&gt;
&lt;p&gt;
Cursor AI는 실무적으로 사용하기에 충분한 기반 코드를 빠르게 생성해주며, 특히 백엔드에서는 제가 사전에 구성해왔던 도메인 기반 설계와 잘 맞았습니다.&lt;br&gt;
Vue는 단순 유지보수만 진행해 왔었고 프로젝트 초기 구축을 진행해 본적이 없었기에 구조나 스타일에 대한 세부 제어는 어려웠지만, CRUD 단위 기능 동작은 빠르게 확인할 수 있어 충분히 프로토타이핑 도구로 활용 가능합니다.
&lt;/p&gt;</description>
      <category>Spring</category>
      <category>cursor ai crud</category>
      <category>cursor ai 프로젝트</category>
      <category>cursor ai 프롬프트</category>
      <category>cursorAI</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/74</guid>
      <comments>https://devhj.tistory.com/74#entry74comment</comments>
      <pubDate>Sun, 11 May 2025 12:13:55 +0900</pubDate>
    </item>
    <item>
      <title>Kafka KRaft 모드 설정 및 오류 해결 가이드</title>
      <link>https://devhj.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 브로커를 KRaft 모드로 실행할 때 meta.properties 파일이 없어 서버가 종료되는 오류를 해결한 과정을 정리합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 오류 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KRaft 모드에서 카프카를 기동하려고 아래 명령을 실행하면:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;bin/kafka-server-start.sh config/kraft/server.properties
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그에 다음과 같은 에러가 발생합니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[2025-04-23 15:22:05,327] WARN No meta.properties file under dir /tmp/kraft-combined-logs/meta.properties (kafka.server.BrokerMetadataCheckpoint)
[2025-04-23 15:22:05,328] ERROR Exiting Kafka due to fatal exception (kafka.Kafka$)
org.apache.kafka.common.KafkaException: No `meta.properties` found in /tmp/kraft-combined-logs (have you run `kafka-storage.sh` to format the directory?)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KRaft 모드에서는 브로커 메타데이터와 로그 디렉터리가 포맷돼 있어야 합니다. 디렉터리에 meta.properties 파일이 없으면 브로커가 시작되지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 절차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;UUID 생성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;cd &amp;lt;KAFKA_HOME&amp;gt; UUID=$(bin/kafka-storage.sh random-uuid) echo &quot;Cluster ID: $UUID&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 파일 확인&lt;/b&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;process.roles=broker,controller
node.id=0
log.dirs=/tmp/kafka-log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;config/kraft/server.properties 파일에서 log.dirs 경로가 포맷할 디렉터리(/tmp/kafka-log 등)와 일치하는지 확인합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스토리지 포맷&lt;/b&gt;이 명령을 실행하면 log.dirs 경로에 meta.properties 파일이 생성됩니다.&lt;/li&gt;
&lt;li&gt;bin/kafka-storage.sh format \\ --cluster-id $UUID \\ --config config/kraft/server.properties&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브로커 기동&lt;/b&gt;이제 오류 없이 Kafka 브로커가 정상 기동됩니다.&lt;/li&gt;
&lt;li&gt;bin/kafka-server-start.sh config/kraft/server.properties&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 추가 팁&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 포맷된 디렉터리를 다시 포맷하려면 -ignore-formatted 옵션을 사용하세요.&lt;/li&gt;
&lt;li&gt;포트 바인딩 여부는 netstat -plnt | grep :9092 명령으로 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;실행 중인 프로세스 확인은 ps -ef | grep kafka.Kafka 명령을 활용하세요.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>no `meta.properties` found in</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/73</guid>
      <comments>https://devhj.tistory.com/73#entry73comment</comments>
      <pubDate>Mon, 5 May 2025 18:56:10 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Outbox Pattern with Spring Event</title>
      <link>https://devhj.tistory.com/72</link>
      <description>&lt;h1&gt;Kafka Outbox Pattern with Spring Event - 주문 예제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Outbox 패턴을 Spring Boot와 Event 기반으로 구현하며, &lt;br /&gt;&lt;b&gt;주문(Order) 도메인&lt;/b&gt;을 중심으로 &lt;b&gt;트랜잭션 안정성 보장&lt;/b&gt;과 &lt;b&gt;Kafka 전송 재시도 가능성&lt;/b&gt;을 확보하는 방법을 설명합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Outbox 패턴은 &lt;b&gt;DB 트랜잭션과 Kafka 전송 간의 일관성을 유지&lt;/b&gt;하기 위한 대표적인 방법입니다.&lt;br /&gt;Spring의 &lt;code&gt;@TransactionalEventListener&lt;/code&gt;를 활용하여 도메인 이벤트를 트랜잭션 내부에서 발행하고, &lt;br /&gt;&lt;b&gt;트랜잭션 커밋 이후 Kafka로 안전하게 발송&lt;/b&gt;되도록 구성했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka Outbox 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Outbox 패턴은 &lt;b&gt;DB 트랜잭션과 Kafka 메시지 발행 사이의 일관성을 보장&lt;/b&gt;하는 구조입니다.&lt;br /&gt;직접 Kafka를 발행하는 대신, 먼저 &lt;b&gt;Outbox 테이블에 메시지를 저장&lt;/b&gt;하고&lt;br /&gt;트랜잭션 커밋 이후 별도 프로세스(리스너 또는 스케줄러)에서 Kafka로 전송합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;장애 발생 시 메시지를 유실하지 않고 재처리할 수 있는 신뢰성 패턴입니다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CDC(Change Data Capture)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDC는 DB의 변경 로그를 추적하여 Kafka로 보내는 &lt;b&gt;비침투형 이벤트 수집 방식&lt;/b&gt;입니다.&lt;br /&gt;반면, Outbox 패턴은 &lt;b&gt;애플리케이션 내부에서 이벤트를 직접 저장하고 처리&lt;/b&gt;합니다.&lt;br /&gt;애플리케이션 코드에서 제어 가능하며, 보다 &lt;b&gt;명시적이고 확장성 있는 흐름&lt;/b&gt;을 구성할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 흐름 요약&lt;/h2&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;OrderService에서 주문을 저장하고 이벤트를 발행한다&lt;/li&gt;
&lt;li&gt;이벤트 리스너가 Outbox 테이블에 이벤트를 저장한다 (BEFORE_COMMIT)&lt;/li&gt;
&lt;li&gt;트랜잭션 커밋 후 Kafka로 메시지를 발송한다 (AFTER_COMMIT)&lt;/li&gt;
&lt;li&gt;Kafka 전송 성공 시 Outbox 상태를 업데이트한다 (sent = true)&lt;/li&gt;
&lt;li&gt;OutboxScheduler가 주기적으로 발행되지 못한 메시지를 재전송한다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 클래스&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. OrderService&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
@Service
@RequiredArgsConstructor
public class OrderService {
    ...
    @Transactional
    public Long createOrder(Long productId, int quantity) {
        ...
        publisher.publishEvent(event); // Spring 이벤트 발행
        return orderSave.getId();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. OutboxBeforeCommitListener&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
@Component
@RequiredArgsConstructor
public class OutboxBeforeCommitListener {
    ...
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void saveOutbox(OrderCreatedEvent event) {
        ...
        outboxRepository.save(outbox);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. OutboxAfterCommitListener + OutBoxPublishService&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
@Component
@RequiredArgsConstructor
public class OutboxAfterCommitListener {
    ...
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void publishToKafka(OrderCreatedEvent event) {
        outBoxPublishService.publish(event);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
@Slf4j
@RequiredArgsConstructor
@Service
public class OutBoxPublishService {
    ...
    public void publish(OrderCreatedEvent event) {
        ...
        kafkaTemplate.send(&quot;order.created&quot;, outbox.getPayload())
            .whenComplete((result, ex) -&amp;gt; {
                if (ex == null) {
                    outbox.completeSent();
                    eventOutboxRepository.save(outbox);
                }
            });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. OutboxScheduler&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
@Slf4j
@Component
@RequiredArgsConstructor
public class OutboxScheduler {
    ...
    @Scheduled(fixedDelay = 5000)
    @Transactional
    public void publishPendingOutboxEvents() {
        ...
        kafkaTemplate.send(&quot;order.created&quot;, outbox.getPayload());
        outbox.completeSent();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;OrderService는 트랜잭션 내에서 주문 저장과 이벤트 발행을 동시에 처리&lt;/li&gt;
&lt;li&gt;BEFORE_COMMIT 리스너는 트랜잭션 커밋 전에 Outbox 테이블에 기록&lt;/li&gt;
&lt;li&gt;AFTER_COMMIT 리스너는 커밋 후 Kafka로 메시지를 발행&lt;/li&gt;
&lt;li&gt;발행 성공 시 sent = true로 상태를 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka에서 메시지 순서 보장 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 &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;KafkaTemplate.send(topic, key, value) 형태로 &lt;b&gt;동일한 키&lt;/b&gt; 사용&lt;/li&gt;
&lt;li&gt;동일 키는 항상 &lt;b&gt;동일 파티션에 할당&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Kafka는 해당 파티션의 메시지를 &lt;b&gt;순서대로 소비&lt;/b&gt;하게 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;시스템 규모가 크고, 메시지 유실 방지 및 재처리가 중요한 경우&lt;/b&gt;&lt;br /&gt;Kafka + Outbox 패턴은 안정성과 신뢰성을 높이는 효과적인 방식입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 소스 코드:&lt;/b&gt; &lt;a href=&quot;https://github.com/devHjlee/kafka-outbox-pattern.git&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; https://github.com/devHjlee/kafka-outbox-pattern.git &lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>kafka oubox</category>
      <category>kafka srping event</category>
      <category>oubox pattern</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/72</guid>
      <comments>https://devhj.tistory.com/72#entry72comment</comments>
      <pubDate>Mon, 5 May 2025 18:53:42 +0900</pubDate>
    </item>
    <item>
      <title>Kafka와 Redis로 만든 선착순 이벤트 시스템 구조 분석</title>
      <link>https://devhj.tistory.com/71</link>
      <description>&lt;h1&gt;Kafka EventRush&lt;/h1&gt;
&lt;p&gt;Kafka + Redis 기반 선착순 이벤트 실습 프로젝트&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/devHjlee/kafka-eventrush&quot;&gt;GitHub 바로가기&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;Kafka와 Redis를 활용한 실시간 선착순 이벤트 입니다. 대규모 사용자 요청이 동시에 들어오는 상황에서 중복 참여를 방지하고, 정해진 인원 수만 이벤트에 당첨되도록 처리하는 구조를 구현했습니다.&lt;/p&gt;
&lt;p&gt;Kafka는 비동기 메시징 큐로 활용되며, Redis는 중복 검사와 선착순 인원 제한에 사용됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;사용 기술&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Java 17  &lt;/li&gt;
&lt;li&gt;Spring Boot 3.3 &lt;/li&gt;
&lt;li&gt;Kafka 3.6.1 (KRaft 모드)  &lt;/li&gt;
&lt;li&gt;Redis  &lt;/li&gt;
&lt;li&gt;MySQL  &lt;/li&gt;
&lt;li&gt;JUnit5, Spring Boot Test  &lt;/li&gt;
&lt;li&gt;k6 (성능 테스트 도구)  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Kafka 간략 소개&lt;/h2&gt;
&lt;p&gt;Kafka는 대용량의 데이터를 빠르고 안정적으로 처리할 수 있는 분산 메시징 시스템입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Producer&lt;/strong&gt;: 메시지를 Kafka로 전송  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Topic&lt;/strong&gt;: 메시지를 저장하는 공간 (파티션으로 병렬화)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer Group&lt;/strong&gt;: 메시지를 읽는 소비자 그룹 (그룹마다 독립적으로 처리)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offset&lt;/strong&gt;: 메시지의 위치 (Consumer가 어디까지 읽었는지를 관리)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;소스 코드&lt;/h2&gt;
&lt;h3&gt;EventJoinService&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequiredArgsConstructor
@Service
public class EventJoinService {

    private final KafkaTemplate&amp;lt;String, EventJoinMessage&amp;gt; kafkaTemplate;
    private final RedisEventJoinService redisEventJoinService;
    private final EventJoinRepository eventJoinRepository;

    public JoinResult requestJoin(Long userId) {
        if (!redisEventJoinService.checkUserAndMark(userId)) {
            return JoinResult.DUPLICATE;
        }

        if (!redisEventJoinService.incrementAndCheckLimit()) {
            return JoinResult.LIMIT_EXCEEDED;
        }

        kafkaTemplate.send(&amp;quot;event.join&amp;quot;, new EventJoinMessage(userId, LocalDateTime.now()));
        return JoinResult.SUCCESS;
    }

    @Transactional
    public void saveEvent(EventJoinMessage eventJoinMessage) {
        EventJoin entity = EventJoin.builder()
                .userId(eventJoinMessage.getUserId())
                .requestTime(eventJoinMessage.getRequestTime())
                .build();
        eventJoinRepository.save(entity);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RedisEventJoinService&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
@RequiredArgsConstructor
public class RedisEventJoinService {

    private static final String USER_SET_KEY = &amp;quot;event:join:users&amp;quot;;
    private static final String COUNTER_KEY = &amp;quot;event:join:counter&amp;quot;;

    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    private static final int MAX_COUNT = 100;

    /**
     * 중복 사용자 여부 확인 (참여 여부 확인)
     */
    public boolean checkUserAndMark(Long userId) {
        Long added = redisTemplate.opsForSet().add(USER_SET_KEY, String.valueOf(userId));
        return added != null &amp;amp;&amp;amp; added &amp;gt; 0; // true면 신규 사용자
    }

    /**
     * 지정된 인원 수를 초과했는지 확인 (INCR 방식)
     */
    public boolean incrementAndCheckLimit() {
        Long currentCount = redisTemplate.opsForValue().increment(COUNTER_KEY);
        return currentCount != null &amp;amp;&amp;amp; currentCount &amp;lt;= MAX_COUNT;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Kafka Producer,Consumer Config&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory&amp;lt;String, EventJoinMessage&amp;gt; eventJoinProducerFactory() {
        Map&amp;lt;String, Object&amp;gt; config = new HashMap&amp;lt;&amp;gt;();
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &amp;quot;localhost:9092&amp;quot;);
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(config);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, EventJoinMessage&amp;gt; eventJoinKafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(eventJoinProducerFactory());
    }
}

@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory&amp;lt;String, EventJoinMessage&amp;gt; eventJoinConsumerFactory() {
        JsonDeserializer&amp;lt;EventJoinMessage&amp;gt; valueDeserializer = new JsonDeserializer&amp;lt;&amp;gt;(EventJoinMessage.class);
        valueDeserializer.addTrustedPackages(&amp;quot;*&amp;quot;);

        Map&amp;lt;String, Object&amp;gt; config = new HashMap&amp;lt;&amp;gt;();
        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &amp;quot;localhost:9092&amp;quot;);
        config.put(ConsumerConfig.GROUP_ID_CONFIG, &amp;quot;event-consumer-group&amp;quot;);
        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, valueDeserializer);

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(config, new StringDeserializer(), valueDeserializer);
    }

    @Bean(name = &amp;quot;eventJoinKafkaListenerContainerFactory&amp;quot;)
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, EventJoinMessage&amp;gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, EventJoinMessage&amp;gt; factory =
                new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(eventJoinConsumerFactory());
        factory.setConcurrency(3);
        return factory;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Kafka Consumer&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequiredArgsConstructor
@Slf4j
@Component
public class EventJoinConsumer {

    private final EventJoinService eventJoinService;

    @KafkaListener(
            topics = &amp;quot;event.join&amp;quot;,
            groupId = &amp;quot;event-consumer-group&amp;quot;,
            containerFactory = &amp;quot;eventJoinKafkaListenerContainerFactory&amp;quot;
    )
    public void consume(ConsumerRecord&amp;lt;String, EventJoinMessage&amp;gt; record) {
        int partition = record.partition();
        long offset = record.offset();
        EventJoinMessage message = record.value();
        log.info(&amp;quot;Partition: {}, Offset: {}, UserId: {}, Time: {}&amp;quot;, partition, offset, message.getUserId(), message.getRequestTime());

        eventJoinService.saveEvent(message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;요청 ~ 저장 흐름&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;1) Client POST /event/join {userId:3}
2) Spring Controller
3) EventJoinService
    - Redis 중복 &amp;amp; 인원 체크
4) Kafka Producer️
5) Kafka Topic (event.join)️
6) Kafka Consumer️
7) MySQL (event_join 테이블 저장)&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;Redis 위치별 장단점&lt;/h2&gt;
&lt;h3&gt;Producer 측 Redis 사용&lt;/h3&gt;
&lt;table&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;- Kafka로 불필요한 메시지 전송 없음&lt;br&gt;- 빠른 사용자 응답 처리 가능&lt;/td&gt;
&lt;td&gt;- Kafka가 메시지 수신하지 못해도 누락 추적 어려움&lt;br&gt;- 로그 기반 복구 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Consumer 측 Redis 사용&lt;/h3&gt;
&lt;table&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;- Kafka를 통해 모든 이벤트 기록이 남아 장애 대응 용이&lt;br&gt;- Retry 및 로그 분석 쉬움&lt;/td&gt;
&lt;td&gt;- Kafka 트래픽 증가 (필요 없는 메시지도 소비)&lt;br&gt;- 실시간 응답 지연 발생 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;프로젝트는 Kafka/Redis 기반의 이벤트 처리 구조를 학습하고, 실전 상황에서의 구조적 의사결정을 연습하기 위해 진행하였습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>kafka redis</category>
      <category>spring kafka</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/71</guid>
      <comments>https://devhj.tistory.com/71#entry71comment</comments>
      <pubDate>Mon, 5 May 2025 18:47:26 +0900</pubDate>
    </item>
    <item>
      <title>Tomcat SSLHandshakeException 오류 해결 후기 (JDK 버전 관련)</title>
      <link>https://devhj.tistory.com/70</link>
      <description>&lt;h1&gt;  Tomcat SSLHandshakeException 오류 해결 후기 (JDK 버전 관련)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수인계 받은 레거시 시스템 2개가 Windows Server에 있었으며 각각 서비스로 등록된 2개의 서비스중 하나의 Tomcat에서 &lt;code&gt;SSLHandshakeException&lt;/code&gt; 오류가 발생했다.&lt;br /&gt;당연히 같은 JDK(1.8.0_101)를 바라보고 있을줄 알았으나, 다른 JDK(1.8.0_045)를 바라보고 있었으며, 서비스 등록 방식의 특성을 몰라 한참을 헤맸다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;하나의 서버에 A, B 두 개의 Tomcat 인스턴스가 독립적으로 실행 중&lt;/li&gt;
&lt;li&gt;두 인스턴스 모두 &lt;code&gt;catalina.bat&lt;/code&gt;, &lt;code&gt;setenv.bat&lt;/code&gt;에 &lt;code&gt;JAVA_HOME = jdk1.8.0_101&lt;/code&gt;로 설정되어 있음&lt;/li&gt;
&lt;li&gt;하지만 A는 오류 발생, B는 정상 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;code&gt;A 톰캣&lt;/code&gt;에서 외부 HTTPS API 요청 시 &lt;code&gt;SSLHandshakeException&lt;/code&gt; 발생&lt;/li&gt;
&lt;li&gt;&lt;code&gt;catalina.out&lt;/code&gt; 로그에는 &lt;code&gt;JDK 1.8.0_045&lt;/code&gt;가 사용 중인 것으로 확인됨&lt;/li&gt;
&lt;li&gt;반면, &lt;code&gt;B 톰캣&lt;/code&gt;은 같은 설정인데도 &lt;code&gt;JDK 1.8.0_101&lt;/code&gt;로 정상 동작&lt;/li&gt;
&lt;/ul&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;JDK 버전&lt;/th&gt;
&lt;th&gt;SSL 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A 톰캣&lt;/td&gt;
&lt;td&gt;1.8.0_045&lt;/td&gt;
&lt;td&gt;SSLHandshakeException 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B 톰캣&lt;/td&gt;
&lt;td&gt;1.8.0_101&lt;/td&gt;
&lt;td&gt;정상 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;code&gt;startup.bat&lt;/code&gt;, &lt;code&gt;catalina.bat&lt;/code&gt;, &lt;code&gt;setenv.bat&lt;/code&gt; 모두 &lt;code&gt;JAVA_HOME = jdk1.8.0_101&lt;/code&gt;로 명시됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;version.bat&lt;/code&gt; 실행 시에도 정상적으로 1.8.0_101로 확인됨&lt;/li&gt;
&lt;li&gt;그런데도 A 톰캣은 여전히 1.8.0_045로 실행됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 JDK 1.8.0_045에서 SSL 오류가 발생했을까?&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;JDK 1.8.0_045&lt;/code&gt;는 2015년 출시된 구버전&lt;/li&gt;
&lt;li&gt;최신 TLS 인증서 체인을 검증하기에 부족한 부분이 있음
&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;일부 TLS 1.2 cipher suite 미지원&lt;/li&gt;
&lt;li&gt;인증서 검증 로직이 현대 환경과 맞지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tomcat이 Windows 서비스로 실행될 경우, 서비스 등록 시점의 JAVA_HOME을 따라감&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;code&gt;service.bat install&lt;/code&gt; 수행 당시, 시스템의 &lt;code&gt;JAVA_HOME&lt;/code&gt;이 &lt;code&gt;1.8.0_045&lt;/code&gt;로 설정되어 있었음&lt;/li&gt;
&lt;li&gt;이후 &lt;code&gt;setenv.bat&lt;/code&gt;, &lt;code&gt;catalina.bat&lt;/code&gt; 등을 수정해도 서비스 등록 당시의 설정은 변경되지 않음&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;Windows 서비스로 등록된 Tomcat은 설정 파일보다 등록 당시의 환경 변수를 우선함&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&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;code class=&quot;language-bat&quot;&gt;service.bat remove&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;환경변수 또는 &lt;code&gt;setenv.bat&lt;/code&gt;에 &lt;code&gt;JAVA_HOME&lt;/code&gt;을 1.8.0_101로 명시&lt;/li&gt;
&lt;li&gt;톰캣 서비스 재등록&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bat&quot;&gt;service.bat install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;톰캣 재시작 후 &lt;code&gt;catalina.out&lt;/code&gt; 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OR&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커맨드에서 톰캣이 설치된 bin 경로 .\tomcat8w.exe //ES//서비스네임을 실행하여 java tap에서 JDK 버전 변경&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pAWJc/btsNsS2kHtE/EN64jhoXRnzKyXROMim0vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pAWJc/btsNsS2kHtE/EN64jhoXRnzKyXROMim0vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pAWJc/btsNsS2kHtE/EN64jhoXRnzKyXROMim0vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpAWJc%2FbtsNsS2kHtE%2FEN64jhoXRnzKyXROMim0vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;411&quot; height=&quot;159&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;code&gt;A 톰캣&lt;/code&gt;도 &lt;code&gt;1.8.0_101&lt;/code&gt;로 정상 인식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SSLHandshakeException&lt;/code&gt; 오류 사라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;톰캣 서비스 등록 시점의 Java 환경 변수 영향&lt;/li&gt;
&lt;li&gt;톰캣이 서비스로 실행될 경우, 이후의 &lt;code&gt;JAVA_HOME&lt;/code&gt; 설정은 반영되지 않음&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java</category>
      <category>SSLHandshakeException</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/70</guid>
      <comments>https://devhj.tistory.com/70#entry70comment</comments>
      <pubDate>Mon, 21 Apr 2025 16:31:21 +0900</pubDate>
    </item>
    <item>
      <title>Enum equals , ==</title>
      <link>https://devhj.tistory.com/69</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영중인 시스템에서 최근들어 NullPointException이 자주 발생하였고 대부분이 Enum의 equals 였습니다.&lt;br /&gt;원인은 DB 컬럼에 디폴트값 설정도 안되어 있는 컬럼과 특정 로직에서 그 컬럼을 Null로 업데이트를 하였고&lt;br /&gt;비교로직에서는 memberDto.joinYn.equals(TypeYn.N) 형식으로 되어있어 발생 했습니다. 이미 턴키 프로젝트 개발자들은 다 떠난상태고&lt;br /&gt;소스레벨까지 제대로 체크하지 못한 죄도 있고... 이렇게 구현된 부분을 전체 수정할겸 어떤형식으로 하는게 좋을지 남기기위해 포스팅 합니다.&lt;/p&gt;
&lt;h5&gt;Enum&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum은 Java에서 상수를 정의하는 특별한 형태의 클래스입니다. Enum 상수들은 클래스로 정의되어 있으며, JVM 메모리에는 다음과 같은 방식으로 로드됩니다.&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;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Enum 클래스는 애플리케이션 실행 시 클래스 로더에 의해 로드됩니다. 클래스 로더는 Enum 클래스를 JVM 메모리에 로드하고, 클래스 정의를 통해 Enum 상수들이 정의됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;힙 영역에 상주&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로드된 Enum 클래스와 그 안에 정의된 Enum 상수들은 힙(heap) 영역에 인스턴스로 생성됩니다. 이 인스턴스들은 프로그램의 수명 동안 메모리에 상주하며, 프로그램 전체에서 공유되어 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Enum Test&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

public class EnumEqualsNPETest {
    @Test
    void checkEnumNpe() {
        // given
        MemberDto memberDto = new MemberDto(&quot;memberA&quot;, null);

        // when then
        // 1.NPE 발생 현재 문제의 소스
        assertThrows(NullPointerException.class, () -&amp;gt; {memberDto.joinYn.equals(TypeYn.N);});
        // 2.NPE 피할수 있음
        assertThat(TypeYn.N.equals(memberDto.joinYn)).isEqualTo(false);
        assertThat(memberDto.joinYn == (TypeYn.N)).isEqualTo(false);

        // Equals 사용시 NPE는 피할수 있지만, 맞지 않는 타입에 대한 오류발견이 늦어 질 수 있음
        assertThat(YnTYpe.Y.equals(memberDto.joinYn)).isEqualTo(false);
        // 타입오류
        //memberDto.joinYn == TypeYn.Y;
    }
}

enum TypeYn {
    Y,N
}

enum YnTYpe {
    Y,N
}

class MemberDto {
    TypeYn joinYn;
    String name;

    public MemberDto(String name,TypeYn joinYn) {
        this.name = name;
        this.joinYn = joinYn;
    }
} &lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;NPE 발생 (현재 문제점)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서는 Enum 상수의 필드가 null인 경우, NullPointerException이 발생하는지를 확인합니다. 이는 Enum 상수의 필드가 null인 경우, 상수를 참조할 때 NullPointerException이 발생할 수 있기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Enum 상수 비교&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서는 Enum 상수 간의 동등성(equality) 및 동일성(identity)을 비교합니다. Enum 상수의 동등성은 equals() 메서드를 사용하여 비교하고, 동일성은 == 연산자를 사용하여 비교합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;유형 일치 확인 (최종 변경)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서는 Enum 상수의 유형(Type)이 일치하지 않는 경우에 대한 검증도 수행합니다. 이는 다른 Enum 유형을 비교할 때 잘못된 타입이면 컴파일시점에 오류를 잡을 수 있습니다.&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;1100&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSnuK3/btsGzbfatqV/douN0IQ0VQprUe2PZshgW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSnuK3/btsGzbfatqV/douN0IQ0VQprUe2PZshgW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSnuK3/btsGzbfatqV/douN0IQ0VQprUe2PZshgW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSnuK3%2FbtsGzbfatqV%2FdouN0IQ0VQprUe2PZshgW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;282&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Java</category>
      <category>enum</category>
      <category>Enum ==</category>
      <category>Enum equals</category>
      <category>Enum Equals ==</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/69</guid>
      <comments>https://devhj.tistory.com/69#entry69comment</comments>
      <pubDate>Thu, 11 Apr 2024 19:51:46 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 모니터링 구축</title>
      <link>https://devhj.tistory.com/68</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 와탭과 같은 모니터링 솔루션을 도입하여 사용하고 있으나,&lt;br /&gt;Spring Boot로 구성한 개인 프로젝트에서는 모니터링을 위해 많이 사용되고 있다는 Spring Actuator + Prometheus + Grafana 조합으로&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;Spring Actuator 기본 설정 + Prometheus + Grafana 적용&lt;/li&gt;
&lt;li&gt;Spring Actuator 보안 적용 (추후 작성 예정)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Actuator&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Actuator는 Spring Boot 애플리케이션의 모니터링과 관리를 간편하게 할 수 있는 기능을 제공합니다.&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;엔드포인트 제공: Spring Actuator는 여러 가지 엔드포인트를 제공하여 애플리케이션의 상태, 메트릭, 환경 설정 등에 대한 정보를 노출합니다. 예를 들어, /health, /info, /metrics, /env, /beans 등의 엔드포인트를 통해 다양한 정보를 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;사용자 정의 엔드포인트: Actuator를 확장하여 사용자 정의 엔드포인트를 생성할 수 있습니다. 이를 통해 애플리케이션에 특화된 추가 정보를 노출하거나 관리 작업을 수행할 수 있습니다.&lt;/li&gt;
&lt;li&gt;실시간 메트릭 수집: Actuator는 애플리케이션의 다양한 메트릭을 실시간으로 수집합니다. CPU 사용량, 메모리 사용량, 요청 수 등의 정보를 제공하여 애플리케이션의 성능을 모니터링할 수 있습니다.&lt;/li&gt;
&lt;li&gt;보안 기능: Spring Actuator는 Spring Security와의 통합을 통해 엔드포인트에 대한 인증 및 권한 부여를 지원합니다. 이를 통해 액세스를 제어하고 보안 취약점을 최소화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;CSRF(Cross-Site Request Forgery) 보호: Actuator 엔드포인트는 기본적으로 CSRF 공격으로부터 보호됩니다. 이는 Spring Security의 CSRF 보호를 통해 구현됩니다.&lt;/li&gt;
&lt;li&gt;다양한 확장성: Actuator는 확장 가능한 구조를 가지고 있으며, 필요에 따라 사용자 지정 기능을 추가하여 확장할 수 있습니다.&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 Actuator는 기본적으로 제공되는 엔드포인트가 있지만, 모든 애플리케이션에 완벽하게 맞지는 않을 수 있습니다. 특정한 요구사항이나 환경에 따라 추가적인 구성이 필요할 수 있습니다.&lt;/li&gt;
&lt;li&gt;보안 설정의 복잡성: Spring Actuator의 보안 설정은 강력하지만, 구성 및 관리가 복잡할 수 있습니다. 특히 엔드포인트에 대한 인증 및 권한 부여 설정을 유연하게 구성하려면 추가적인 작업이 필요할 수 있습니다.&lt;/li&gt;
&lt;li&gt;리소스 사용: Actuator를 사용하여 애플리케이션의 상태 및 메트릭을 모니터링하면 약간의 추가 리소스가 필요할 수 있습니다. 따라서 대규모 애플리케이션의 경우 이를 고려해야 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;보안 취약성: Actuator 엔드포인트가 노출되는 경우, 악의적인 사용자에게 민감한 정보가 노출될 수 있습니다. 이에 대한 보안 대책을 취해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github 링크 : &lt;a href=&quot;https://github.com/devHjlee/Blog-Spring/tree/master/springActuator&quot;&gt;SpringActuator 예제&lt;/a&gt;&lt;/h2&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;spring boot : 3.2.4&lt;/li&gt;
&lt;li&gt;OpenJdk 17&lt;/li&gt;
&lt;li&gt;Gradle 에 implementation 'org.springframework.boot:spring-boot-starter-actuator' 추가&lt;/li&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;li&gt;Grafana&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SpringBoot Application.properties&lt;/h2&gt;
&lt;div&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block; text-align: center;&quot; data-ad-layout=&quot;in-article&quot; data-ad-format=&quot;fluid&quot; data-ad-client=&quot;ca-pub-1657291406799293&quot; data-ad-slot=&quot;6047005770&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# Actuator 관리용 서버 포트 설정
management.server.port=8888
# Actuator 엔드포인트의 기본 경로 설정
management.endpoints.web.base-path=/mnt
# JMX 노출 설정
management.endpoints.jmx.exposure.exclude=*
# 웹 엔드포인트 노출 설정
management.endpoints.web.exposure.include=*
# 환경 정보를 제공하는 엔드포인트
# 데이터베이스 비밀번호 또는 API 키와 같은 중요한 정보가 포함될 수 있습니다. 이러한 정보가 노출되면 보안 문제가 발생할 수 있으므로, 이 설정을 사용할 때에는 주의가 필요합니다.
management.endpoint.env.show-values=always
# 애플리케이션의 정보를 제공
management.endpoint.info.enabled=true&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;각 개별 endpoint 활성화하거나 비활성화 하고 HTTP 또는 JMX를 통해 노출(원격 액세스 가능)할 수 있습니다.&lt;/li&gt;
&lt;li&gt;기본 제공 엔드포인트는 사용 가능한 경우에만 자동 구성됩니다.&lt;/li&gt;
&lt;li&gt;대부분의 애플리케이션은 설정한 엔드포인트 경로 (예시 /mnt) URL에 매핑되는 HTTP를 통한 노출을 선택합니다. 예를 들어 기본적으로 health엔드포인트는 에 매핑됩니다 /actuator/health.&lt;/li&gt;
&lt;li&gt;/mnt 로 접근시 acuator 설정에 맞춰 접근 가능한 URL을 보여줍니다. 현재는 예시를 위해 전부 노출해놨으나 각자 보안을 위해 설정을 변경해야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;actuator1.png&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSylW6/btsGwBLaTqC/d15TDvkwuYhuNvDZJaWcEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSylW6/btsGwBLaTqC/d15TDvkwuYhuNvDZJaWcEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSylW6/btsGwBLaTqC/d15TDvkwuYhuNvDZJaWcEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSylW6%2FbtsGwBLaTqC%2Fd15TDvkwuYhuNvDZJaWcEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;573&quot; height=&quot;743&quot; data-filename=&quot;actuator1.png&quot; data-origin-width=&quot;573&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;보안의 예시로 위에 설정중 management.endpoint.env.show-values=always (default : never) 하고 /mnt/env 에 접근을 하면 환경설정으로 해놓았던 정보까지 전부 보여집니다.&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;811&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2ayuS/btsGwfn6fZg/YMOyVfK90CIXSTBPgFIrk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2ayuS/btsGwfn6fZg/YMOyVfK90CIXSTBPgFIrk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2ayuS/btsGwfn6fZg/YMOyVfK90CIXSTBPgFIrk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2ayuS%2FbtsGwfn6fZg%2FYMOyVfK90CIXSTBPgFIrk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;434&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;434&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;h2 data-ke-size=&quot;size26&quot;&gt;Prometheus (&lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/getting_started/&quot;&gt;https://prometheus.io/docs/prometheus/latest/getting_started/&lt;/a&gt;)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 오픈 소스 시계열 데이터베이스 및 모니터링 시스템으로, 서버 및 애플리케이션의 성능 및 상태를 실시간으로 모니터링하고 경고를 생성하는 데 사용됩니다.&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;강력한 쿼리 언어: PromQL은 데이터를 선택하고 집계하는 데 유용한 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;경량화: 프로메테우스는 경량화된 설계로 메모리 효율적인 모니터링을 가능하게 합니다.&lt;/li&gt;
&lt;li&gt;확장성: 수평 확장을 지원하여 대규모 환경에서도 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Pull: Pull 방식의 구조를 채택해서 모든 메트릭 정보를 중앙 서버로 보내지 않아도 됩니다.&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;실시간 데이터 처리: 대규모 환경에서 실시간 데이터 처리에 제한이 있을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;prometheus.yml 예시&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;global:
scrape_interval: 15s

scrape_configs:
- job_name: &quot;springActuator&quot;
  static_configs:
  - targets: [&quot;localhost:8888&quot;]
    metrics_path: '/mnt/prometheus'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정 파일을 수정 후 프로메테우스 (localhost:9090)에 접근하여 연결한 서버의 상태 정보를 확인 할 수 있습니다.&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;1902&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn3gtB/btsGtL250X1/by1CCxodmBz7hwJv4UC68K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn3gtB/btsGtL250X1/by1CCxodmBz7hwJv4UC68K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn3gtB/btsGtL250X1/by1CCxodmBz7hwJv4UC68K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn3gtB%2FbtsGtL250X1%2Fby1CCxodmBz7hwJv4UC68K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1902&quot; height=&quot;422&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;422&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;Actuator 에서 제공하는 /prometheus 엔드포인트를 통해 metirc 에 대한 정보를 수집합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Grafana&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grafana는 오픈 소스의 데이터 시각화 및 모니터링 도구로, 다양한 데이터 원본에서 데이터를 쿼리하고 시각적으로 표현하는 기능을 제공합니다.&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;사용자 친화적: Grafana는 직관적이고 사용하기 쉬운 사용자 인터페이스를 제공하여 사용자가 쉽게 대시보드를 만들고 시각화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;다양한 데이터 소스 지원: InfluxDB, OpenTSDB, AWS CloudWatch, Azure Monitor, ElasticSearch 등 데이터 소스에서 데이터를 가져와 시각화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;확장성: Grafana는 플러그인 시스템과 API를 통해 사용자가 시스템을 확장하고 사용자 정의할 수 있는 높은 확장성을 제공합니다.&lt;/li&gt;
&lt;li&gt;커뮤니티 및 지원: Grafana는 활발한 커뮤니티와 다양한 지원 자료를 통해 사용자들이 도움을 받을 수 있습니다.&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;리소스 사용량: 대규모 대시보드 또는 데이터 소스를 사용할 경우 Grafana가 많은 시스템 리소스를 사용할 수 있습니다.&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;설치 후 첫 로그인시 admin / admin 을 입력 후 비밀번호룰 변경 해야합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus 연결 (인증에 대한 설정은 다음글에서...)&lt;/li&gt;
&lt;li&gt;Add new connection 에서 Prometheus 를 선택 후 Prometheus 서버 정보 기입&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;2555&quot; data-origin-height=&quot;791&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bblUza/btsGtKpAhbH/MEUwkr3q3TcrW1FSvMHTGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bblUza/btsGtKpAhbH/MEUwkr3q3TcrW1FSvMHTGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bblUza/btsGtKpAhbH/MEUwkr3q3TcrW1FSvMHTGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbblUza%2FbtsGtKpAhbH%2FMEUwkr3q3TcrW1FSvMHTGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2555&quot; height=&quot;791&quot; data-origin-width=&quot;2555&quot; data-origin-height=&quot;791&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;1319&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWSQWq/btsGu6SIbOU/eSU3bg3Lbph98fNOkWy7f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWSQWq/btsGu6SIbOU/eSU3bg3Lbph98fNOkWy7f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWSQWq/btsGu6SIbOU/eSU3bg3Lbph98fNOkWy7f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWSQWq%2FbtsGu6SIbOU%2FeSU3bg3Lbph98fNOkWy7f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1319&quot; height=&quot;912&quot; data-origin-width=&quot;1319&quot; data-origin-height=&quot;912&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;1259&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nw7aZ/btsGuKPVoEb/MObkK5YAQWqSWbrie9iRR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nw7aZ/btsGuKPVoEb/MObkK5YAQWqSWbrie9iRR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nw7aZ/btsGuKPVoEb/MObkK5YAQWqSWbrie9iRR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnw7aZ%2FbtsGuKPVoEb%2FMObkK5YAQWqSWbrie9iRR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1259&quot; height=&quot;818&quot; data-origin-width=&quot;1259&quot; data-origin-height=&quot;818&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;메인에서 새 DashBoard 를 추가 후 Uptime 에 대한 metric 을 추가&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-filename=&quot;grafanaset4.png&quot; data-origin-width=&quot;1885&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SUHsM/btsGwyHFDh0/Dz6k7zJ3QKAEf9XNpkm6sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SUHsM/btsGwyHFDh0/Dz6k7zJ3QKAEf9XNpkm6sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SUHsM/btsGwyHFDh0/Dz6k7zJ3QKAEf9XNpkm6sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSUHsM%2FbtsGwyHFDh0%2FDz6k7zJ3QKAEf9XNpkm6sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1885&quot; height=&quot;799&quot; data-filename=&quot;grafanaset4.png&quot; data-origin-width=&quot;1885&quot; data-origin-height=&quot;799&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;div&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block; text-align: center;&quot; data-ad-layout=&quot;in-article&quot; data-ad-format=&quot;fluid&quot; data-ad-client=&quot;ca-pub-1657291406799293&quot; data-ad-slot=&quot;6047005770&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uvqlt/btsGwwpxuXF/XG3klA5zq8T67kOyPkQgN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uvqlt/btsGwwpxuXF/XG3klA5zq8T67kOyPkQgN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uvqlt/btsGwwpxuXF/XG3klA5zq8T67kOyPkQgN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUvqlt%2FbtsGwwpxuXF%2FXG3klA5zq8T67kOyPkQgN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2540&quot; height=&quot;790&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;790&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;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html?query=health%27%20target=_blank%3E%3Cb%3Ehealth%3C/b%3E%3C/a%3E-groups#actuator.enabling&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html?query=health%27%20target=_blank%3E%3Cb%3Ehealth%3C/b%3E%3C/a%3E-groups#actuator.enabling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/actuator-api/htmlsingle/#overview&quot;&gt;https://docs.spring.io/spring-boot/docs/current/actuator-api/htmlsingle/#overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yjkim97.tistory.com/51&quot;&gt;https://yjkim97.tistory.com/51&lt;/a&gt; (Prometheus Config 설명)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grafana.com/grafana/dashboards/&quot;&gt;https://grafana.com/grafana/dashboards/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>actuator prometheus grafana</category>
      <category>Spring Actuator</category>
      <category>Spring boot Actuator</category>
      <category>spring boot 모니터링</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/68</guid>
      <comments>https://devhj.tistory.com/68#entry68comment</comments>
      <pubDate>Tue, 9 Apr 2024 14:59:10 +0900</pubDate>
    </item>
    <item>
      <title>SpringBoot Gradle</title>
      <link>https://devhj.tistory.com/67</link>
      <description>&lt;h2&gt;Gradle&lt;/h2&gt;
&lt;p&gt;Gradle은 프로젝트를 빌드하고 관리하기 위한 오픈 소스 빌드 자동화 도구입니다. Groovy 또는 Kotlin을 기반으로 하는 DSL(Domain Specific Language)을 사용하여 빌드 스크립트를 작성하며, 안드로이드 애플리케이션부터 Java, Kotlin, 그루비 등의 프로젝트까지 다양한 유형의 프로젝트를 지원합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btkPiV/btsF40fejwd/fPfzTIHdwKhOutMVoJ9Wok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btkPiV/btsF40fejwd/fPfzTIHdwKhOutMVoJ9Wok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btkPiV/btsF40fejwd/fPfzTIHdwKhOutMVoJ9Wok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtkPiV%2FbtsF40fejwd%2FfPfzTIHdwKhOutMVoJ9Wok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1488&quot; height=&quot;606&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;특징&lt;ul&gt;
&lt;li&gt;유연성: Gradle은 Groovy나 Kotlin과 같은 DSL(Domain Specific Language)을 사용하여 빌드 스크립트를 작성할 수 있습니다. 이는 빌드 프로세스를 세밀하게 제어할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;선언적 DSL: Gradle은 선언적 DSL을 사용하여 프로젝트의 의존성, 작업 등을 정의할 수 있습니다. 이는 빌드 스크립트를 간단하게 만들어 줍니다.&lt;/li&gt;
&lt;li&gt;멀티 프로젝트 지원: Gradle은 다중 프로젝트 구성을 쉽게 관리할 수 있는 기능을 제공합니다. 이를 통해 대규모 프로젝트의 빌드를 효율적으로 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;자동 의존성 해결: Gradle은 프로젝트의 의존성을 자동으로 해결해주는 기능을 제공합니다. Maven과 유사한 방식으로 중앙 저장소에서 의존성을 다운로드하고 관리합니다.&lt;/li&gt;
&lt;li&gt;플러그인 생태계: Gradle은 다양한 플러그인을 제공하고 있으며, 커뮤니티에서도 다양한 플러그인을 제작하여 공유하고 있습니다. 이를 통해 빌드 프로세스를 확장하고 맞춤화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점&lt;ul&gt;
&lt;li&gt;성능: Gradle은 빌드 속도가 빠르고 효율적입니다. build incrementality 와 캐시 기능을 통해 반복 빌드 시간을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;유연성과 확장성: Gradle은 다양한 프로젝트에 유연하게 적용될 수 있습니다. 작은 프로젝트부터 큰 기업용 소프트웨어까지 다양한 요구 사항을 충족시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;커뮤니티 및 지원: Gradle은 활발한 커뮤니티와 다양한 문서, 튜토리얼, 그리고 지원을 제공합니다. 이는 사용자가 문제를 해결하고 지식을 공유하는 데 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점:&lt;ul&gt;
&lt;li&gt;학습 곡선: Groovy나 Kotlin에 익숙하지 않은 경우 초기 설정 및 스크립트 작성이 어려울 수 있습니다.&lt;/li&gt;
&lt;li&gt;설정의 복잡성: 복잡한 프로젝트 구성의 경우 Gradle 스크립트가 길고 복잡해질 수 있습니다.&lt;/li&gt;
&lt;li&gt;변화의 빈도: Gradle은 활발하게 업데이트되고 개선되는 도구이지만, 때로는 이로 인해 API 변경이나 호환성 문제가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Gradle Wrapper (&lt;a href=&quot;https://docs.gradle.org/current/userguide/gradle_wrapper.html&quot;&gt;https://docs.gradle.org/current/userguide/gradle_wrapper.html&lt;/a&gt;)&lt;/h2&gt;
&lt;p&gt;Gradle 빌드 도구의 버전 관리와 프로젝트 환경 설정을 단순화하는 도구입니다. 이를 통해 개발자들은 Gradle을 미리 설치할 필요 없이 프로젝트에 포함된 Wrapper 스크립트를 실행하여 Gradle을 사용할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동작&lt;ul&gt;
&lt;li&gt;Wrapper 스크립트 설정: Gradle 프로젝트의 루트 디렉토리에는 Gradle Wrapper에 필요한 설정 파일인 gradle-wrapper.properties와 Wrapper 스크립트인 gradlew (또는 Windows에서는 gradlew.bat)가 포함되어 있습니다.&lt;/li&gt;
&lt;li&gt;Gradle 다운로드: 처음 프로젝트를 빌드할 때 Wrapper 스크립트를 실행하면, 지정된 Gradle 버전이 현재 시스템에 설치되어 있는지 확인하고, 없다면 해당 버전의 Gradle을 인터넷에서 다운로드하여 프로젝트 디렉토리에 설치합니다.&lt;/li&gt;
&lt;li&gt;로컬 Gradle 사용: Gradle을 다운로드하고 설치한 후, Wrapper 스크립트는 해당 버전의 Gradle을 사용하여 프로젝트를 빌드하고 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점&lt;ul&gt;
&lt;li&gt;버전 관리: Gradle Wrapper를 사용하면 프로젝트별로 Gradle 버전을 명시적으로 지정할 수 있습니다. 따라서 다른 프로젝트 간에 Gradle 버전 충돌을 방지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;설치 필요 없음: 개발자들은 Gradle을 별도로 설치할 필요가 없습니다. 프로젝트에 포함된 Wrapper를 실행하면 자동으로 필요한 Gradle 버전이 다운로드되어 사용됩니다.&lt;/li&gt;
&lt;li&gt;초기 설정 간편화: 새로운 개발 환경에서 프로젝트를 시작할 때, Gradle Wrapper를 사용하면 Gradle을 설치하고 환경 설정을 할 필요 없이 바로 빌드할 수 있습니다.&lt;/li&gt;
&lt;li&gt;유지 보수 용이성: Gradle Wrapper를 사용하면 프로젝트를 공유하거나 이관할 때 Gradle 버전이 호환되는지 걱정할 필요가 없습니다. 모든 개발자가 동일한 Gradle 버전을 사용하여 일관된 개발 환경을 유지할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Github 링크 : &lt;a href=&quot;https://github.com/devHjlee/Blog-Spring/tree/master/gradle&quot;&gt;SpringBoot Gradle 예제&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;예제&lt;/h3&gt;
&lt;h4&gt;build.gradle (&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/job/running.html&quot;&gt;https://docs.spring.io/spring-batch/reference/job/running.html&lt;/a&gt;)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;// 외부 플러그인을 프로젝트에 적용하는 데 사용됩니다. 이 블록을 사용하여 프로젝트에 필요한 플러그인을 선언하고 버전을 지정할 수 있습니다.
// 각 플러그인은 고유한 id로 식별됩니다. java, org.springframework.boot, io.spring.dependency-management
plugins {
    id &amp;#39;java&amp;#39;
    id &amp;#39;org.springframework.boot&amp;#39; version &amp;#39;3.2.4&amp;#39;
    id &amp;#39;io.spring.dependency-management&amp;#39; version &amp;#39;1.1.4&amp;#39;
}

group = &amp;#39;com.gradle&amp;#39; // 프로젝트 그룹
version = &amp;#39;0.0.2-SNAPSHOT&amp;#39; // 프로젝트의 버전

java { // Java 소스코드의 호환성 버전 설정
    sourceCompatibility = &amp;#39;17&amp;#39;
}

// 프로젝트의 의존성을 해결하는 데 필요한 외부 저장소를 지정하는 데 사용됩니다.
// 프로젝트에서 사용되는 라이브러리 및 플러그인의 찾을 위치를 지정 합니다.
repositories {
    mavenCentral()
    google()
}

// 프로젝트가 의존하는 외부 라이브러리 및 모듈을 선언하는 데 사용됩니다.
// 이 블록은 프로젝트가 컴파일되고 실행될 때 필요한 다른 라이브러리들과의 의존성을 명시적으로 정의합니다.
dependencies {
    // implementation: 프로덕션 코드에 필요한 의존성을 정의합니다.
    implementation &amp;#39;org.springframework.boot:spring-boot-starter-web&amp;#39;
    // compileOnly: 컴파일 시에만 필요한 의존성을 정의합니다. 런타임에는 필요하지 않습니다.
    compileOnly &amp;#39;org.projectlombok:lombok&amp;#39;
    // runtimeOnly: 런타임 시에만 필요한 의존성을 정의합니다. 컴파일 시에는 필요하지 않습니다.
    runtimeOnly &amp;#39;com.mysql:mysql-connector-j&amp;#39;
    // annotationProcessor: 어노테이션 프로세서 의존성을 정의합니다. 주로 롬복과 같은 라이브러리를 사용할 때 필요합니다.
    annotationProcessor &amp;#39;org.projectlombok:lombok&amp;#39;
    // testImplementation: 테스트 코드에 필요한 의존성을 정의합니다.
    testImplementation &amp;#39;org.springframework.boot:spring-boot-starter-test&amp;#39;
}

tasks.named(&amp;#39;test&amp;#39;) {
    useJUnitPlatform()
    println &amp;#39;실행 단계에서 마지막에 실행된다.&amp;#39;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;참고&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;공홈 : &lt;a href=&quot;https://docs.gradle.org/current/userguide/userguide.html&quot;&gt;https://docs.gradle.org/current/userguide/userguide.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;api implementaion 차이 : &lt;a href=&quot;https://mangkyu.tistory.com/296&quot;&gt;https://mangkyu.tistory.com/296&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;task : &lt;a href=&quot;https://velog.io/@hyemin916/Gradle%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-2-Gradle-%EB%B9%8C%EB%93%9C-%EC%A7%9C%EB%B3%B4%EA%B8%B0&quot;&gt;https://velog.io/@hyemin916/Gradle%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-2-Gradle-%EB%B9%8C%EB%93%9C-%EC%A7%9C%EB%B3%B4%EA%B8%B0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>Gradle</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/67</guid>
      <comments>https://devhj.tistory.com/67#entry67comment</comments>
      <pubDate>Tue, 26 Mar 2024 22:03:57 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch</title>
      <link>https://devhj.tistory.com/66</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Batch&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치(Spring Batch)는 스프링 프레임워크 기반으로 개발된 배치 처리 프레임워크입니다. 배치 처리란 대량의 데이터를 일괄적으로 처리하는 작업을 말합니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순한 설정: 스프링 배치는 스프링 프레임워크와 통합되어 있어서, 스프링의 IoC(Inversion of Control) 컨테이너와 의존성 주입(Dependency Injection) 등의 기능을 활용할 수 있습니다. 이를 통해 간단한 설정으로 배치 작업을 구성할 수 있습니다.&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;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;li&gt;관리 및 모니터링: 배치 작업의 실행 상태를 관리하고 모니터링할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습 곡선: 스프링 배치를 사용하기 위해서는 스프링 프레임워크에 대한 이해가 필요합니다. 따라서 초기 학습 곡선이 존재할 수 있습니다.&lt;/li&gt;
&lt;li&gt;설정 복잡성: 복잡한 배치 작업을 구성할 때 설정이 복잡해질 수 있습니다.SpringBatch 주요 단계&lt;/li&gt;
&lt;/ul&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;스프링 배치에서 실행되는 하나의 배치 작업을 나타냅니다.&lt;/li&gt;
&lt;li&gt;Job은 여러 단계(Step)로 구성될 수 있습니다.&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;Job을 구성하는 하위 단위로, 실제로 작업을 수행하는 단계입니다.&lt;/li&gt;
&lt;li&gt;각 단계는 읽기(Read), 처리(Processing), 쓰기(Write) 단계로 구성될 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk 기반 스텝
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk 기반 스텝은 일괄 처리 작업을 아이템 단위로 처리하는 방식입니다.&lt;/li&gt;
&lt;li&gt;일정량(chunk size)의 아이템을 읽어와서 처리하고, 그 결과를 다시 저장합니다.&lt;/li&gt;
&lt;li&gt;일반적으로 대용량의 데이터를 처리할 때 사용됩니다.&lt;/li&gt;
&lt;li&gt;Chunk 기반 스텝은 ItemReader,ItemProcessor,ItemWriter 로 구성됩니다.
&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;Step에서 데이터를 읽어오는 역할을 합니다.&lt;/li&gt;
&lt;li&gt;다양한 데이터 소스에서 데이터를 읽어올 수 있습니다.&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;읽어온 데이터를 가공하거나 필터링하는 역할을 합니다.&lt;/li&gt;
&lt;li&gt;선택적으로 사용될 수 있습니다.&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;처리된 데이터를 저장하거나 외부 시스템에 전달하는 역할을 합니다.&lt;/li&gt;
&lt;li&gt;처리된 아이템을 영속화하는 역할을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tasklet 기반 스텝:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tasklet 기반 스텝은 단일한 작업(Tasklet)을 실행하는 방식입니다.&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JobLauncher (작업 실행기)
&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;Job 실행 요청을 받아들이고, Job을 시작하며, Job 실행 상태를 추적합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JobRepository (작업 저장소):
&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;스프링 배치의 내부 상태와 실행 이력을 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JobInstance (작업 인스턴스):
&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;Job의 실행을 추적하고 관리하는 데 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&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;Job의 각 실행을 나타냅니다.&lt;/li&gt;
&lt;li&gt;Job이 실행될 때마다 새로운 JobExecution이 생성됩니다.&lt;/li&gt;
&lt;li&gt;JobExecution은 Job의 실행 상태와 결과를 포함합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;StepExecution (단계 실행):
&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;Step이 실행될 때마다 새로운 StepExecution이 생성됩니다.&lt;/li&gt;
&lt;li&gt;StepExecution은 Step의 실행 상태와 결과를 포함합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ExecutionContext (실행 컨텍스트):
&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;Job 또는 Step의 실행 도중에 데이터를 유지하고 전달하는 데 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SpringBatch 메타 테이블&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZFCaX/btsF1i1qhhd/TAr0ErcmoESPrgKGOiVLxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZFCaX/btsF1i1qhhd/TAr0ErcmoESPrgKGOiVLxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZFCaX/btsF1i1qhhd/TAr0ErcmoESPrgKGOiVLxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZFCaX%2FbtsF1i1qhhd%2FTAr0ErcmoESPrgKGOiVLxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;902&quot; height=&quot;746&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;746&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://docs.spring.io/spring-batch/reference/schema-appendix.html#exampleDDLScripts&quot;&gt;https://docs.spring.io/spring-batch/reference/schema-appendix.html#exampleDDLScripts&lt;/a&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;BATCH_JOB_INSTANCE
&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;Job의 이름과 파라미터를 저장합니다.&lt;/li&gt;
&lt;li&gt;Job 파라미터에 따라 각각의 JobInstance가 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION
&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;Job이 실행될 때마다 새로운 레코드가 생성되며, 각 레코드는 하나의 JobExecution을 나타냅니다.&lt;/li&gt;
&lt;li&gt;Job의 시작 및 종료 시간, 실행 상태 등의 정보를 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_STEP_EXECUTION
&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;Step이 실행될 때마다 새로운 레코드가 생성되며, 각 레코드는 하나의 StepExecution을 나타냅니다.&lt;/li&gt;
&lt;li&gt;Step의 시작 및 종료 시간, 실행 상태 등의 정보를 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_JOB_EXECUTION_CONTEXT
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job 실행 중에 ExecutionContext를 저장하는 테이블입니다.&lt;/li&gt;
&lt;li&gt;Job이 실행될 때 생성되는 ExecutionContext의 내용을 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_STEP_EXECUTION_CONTEXT
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step 실행 중에 ExecutionContext를 저장하는 테이블입니다.&lt;/li&gt;
&lt;li&gt;Step이 실행될 때 생성되는 ExecutionContext의 내용을 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BATCH_JOB_PARAMS
&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;Job 실행 시 전달된 파라미터의 내용을 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github 링크 : &lt;a href=&quot;https://github.com/devHjlee/Blog-Spring/tree/master/springBatch&quot;&gt;SpringBatch 예제&lt;/a&gt;&lt;/h2&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;spring boot : 3.2.3&lt;/li&gt;
&lt;li&gt;OpenJdk 17&lt;/li&gt;
&lt;li&gt;Mysql 8.0&lt;/li&gt;
&lt;li&gt;Spring Batch 5.1.1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQQtda/btsF1aI9k8I/XDOWM78ThWnluF8ZQ1VxH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQQtda/btsF1aI9k8I/XDOWM78ThWnluF8ZQ1VxH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQQtda/btsF1aI9k8I/XDOWM78ThWnluF8ZQ1VxH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQQtda%2FbtsF1aI9k8I%2FXDOWM78ThWnluF8ZQ1VxH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;399&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block; text-align: center;&quot; data-ad-layout=&quot;in-article&quot; data-ad-format=&quot;fluid&quot; data-ad-client=&quot;ca-pub-1657291406799293&quot; data-ad-slot=&quot;6047005770&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 소스&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TestController (&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/job/running.html&quot;&gt;https://docs.spring.io/spring-batch/reference/job/running.html&lt;/a&gt;)&lt;/h4&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class TestController {
    private final JobLauncher jobLauncher;
    private final JobRegistry jobRegistry;

    @RequestMapping(&quot;/test&quot;)
    public void test(@RequestParam String jobName,@RequestParam String reqDt) throws Exception {
        Job processJob = jobRegistry.getJob(jobName);
        JobParameters jobParameters = new JobParametersBuilder().addString(&quot;reqDt&quot;,reqDt).toJobParameters();

        jobLauncher.run(processJob, jobParameters);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDVndl/btsF4Asjyx5/3FkTUV4ymZVHKFWePJqzA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDVndl/btsF4Asjyx5/3FkTUV4ymZVHKFWePJqzA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDVndl/btsF4Asjyx5/3FkTUV4ymZVHKFWePJqzA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDVndl%2FbtsF4Asjyx5%2F3FkTUV4ymZVHKFWePJqzA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;344&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;344&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;스프링 배치를 실행하는 방법으로는 컨트롤러(Controller)를 활용하는 방법과 커맨드 라인(Command Line)을 통해 실행하는 방법이 있고 예시는 컨트롤러를 통해 구현했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SimpleJobConfig (&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/tasklet.html&quot;&gt;https://docs.spring.io/spring-batch/reference/step/tasklet.html&lt;/a&gt;)&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
@Slf4j
@Configuration
public class SimpleJobConfig {
    @Bean
    public Job simpleJob(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new JobBuilder(&quot;simpleJob&quot;, jobRepository)
                .start(simpleStepOne(null,jobRepository, transactionManager))
                .next(simpleStepTwo(jobRepository, transactionManager))
                .build();
    }

    @Bean
    @JobScope
    public Step simpleStepOne(@Value(&quot;#{jobParameters[reqDt]}&quot;) String requestDate, JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&quot;simpleStepOne&quot;, jobRepository)
                .tasklet((StepContribution contribution, ChunkContext chunkContext) -&amp;gt; {
                    log.info(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; This is simpleStepOne {}&quot;,requestDate);

                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }

    @Bean
    public Step simpleStepTwo(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&quot;simpleStepTwo&quot;, jobRepository)
                .tasklet((StepContribution contribution, ChunkContext chunkContext) -&amp;gt; {
                    log.info(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; simpleStepTwo&quot;);

                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;159&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byM2LH/btsF3Vi7jWd/KamzOXmxc35TrZT7gB6lsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byM2LH/btsF3Vi7jWd/KamzOXmxc35TrZT7gB6lsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byM2LH/btsF3Vi7jWd/KamzOXmxc35TrZT7gB6lsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyM2LH%2FbtsF3Vi7jWd%2FKamzOXmxc35TrZT7gB6lsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;159&quot; height=&quot;344&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;159&quot; data-origin-height=&quot;344&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;simpleJob: simpleJob이라는 이름의 Job 빈을 생성합니다. 이 Job은 JobBuilder를 사용하여 정의되며, 두 개의 Step으로 구성됩니다. start 메서드를 사용하여 첫 번째 Step을 정의하고, next 메서드를 사용하여 두 번째 Step을 연결합니다.&lt;/li&gt;
&lt;li&gt;simpleStepOne,Two: Step 빈을 생성합니다. 이 Step은 Tasklet 기반으로 동작하며, jobParameters[reqDt]를 사용하여 jobParameters에서 reqDt 값을 가져와서 사용합니다. 각 Step은 Tasklet을 실행하고 작업이 완료되면 RepeatStatus.FINISHED를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@JopScope&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job 내에서 한 번만 생성되며, Job이 실행될 때마다 새로운 인스턴스가 생성됩니다.&lt;/li&gt;
&lt;li&gt;Job 내의 여러 Step에서 해당 빈을 공유하고자 할 때 유용합니다.&lt;/li&gt;
&lt;li&gt;@StepScope&lt;/li&gt;
&lt;li&gt;Step이 실행될 때마다 새로운 인스턴스가 생성됩니다.&lt;/li&gt;
&lt;li&gt;각 Step 내에서 해당 빈이 필요할 때마다 새로운 인스턴스를 생성하여 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Slf4j
@Configuration
public class FlowJobConfig {

  @Bean
  public Job flowJob(JobRepository jobRepository, Step stepOne, Step stepTwo, Step stepThree) {
      return new JobBuilder(&quot;flowJob&quot;, jobRepository)
              .start(stepOne)                             // StepOne으로 시작
              .on(&quot;*&quot;)                                    // 모든 결과에 대해서
              .to(stepTwo)                               // StepTwo로 이동
              .from(stepOne).on(&quot;FAILED&quot;).to(stepThree)  // StepOne이 실패한 경우에만 StepThree로 이동
              .end()                                     // Flow 종료
              .build();
  }

  @Bean
  @JobScope
  public Step stepOne(@Value(&quot;#{jobParameters[reqDt]}&quot;) String requestDate, JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder(&quot;flowJob_stepOne&quot;, jobRepository)
              .tasklet((StepContribution contribution, ChunkContext chunkContext) -&amp;gt; {
                  log.info(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; This is flowJob_stepOne {}&quot;,requestDate);

                  contribution.setExitStatus(ExitStatus.FAILED);

                  return RepeatStatus.FINISHED;
              }, transactionManager)
              .build();
  }

  @Bean
  @JobScope
  public Step stepTwo(@Value(&quot;#{jobParameters[reqDt]}&quot;) String requestDate, JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder(&quot;flowJob_stepTwo&quot;, jobRepository)
              .tasklet((StepContribution contribution, ChunkContext chunkContext) -&amp;gt; {
                  log.info(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; This is flowJob_stepTwo {}&quot;,requestDate);

                  return RepeatStatus.FINISHED;
              }, transactionManager)
              .build();
  }

  @Bean
  @JobScope
  public Step stepThree(@Value(&quot;#{jobParameters[reqDt]}&quot;) String requestDate, JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder(&quot;flowJob_stepThree&quot;, jobRepository)
              .tasklet((StepContribution contribution, ChunkContext chunkContext) -&amp;gt; {
                  log.info(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; This is flowJob_stepThree {}&quot;,requestDate);

                  return RepeatStatus.FINISHED;
              }, transactionManager)
              .build();
  }
}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DH15x/btsF1Mnq8sQ/pIH3RIz6n3Yd5XKEKMgEk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DH15x/btsF1Mnq8sQ/pIH3RIz6n3Yd5XKEKMgEk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DH15x/btsF1Mnq8sQ/pIH3RIz6n3Yd5XKEKMgEk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDH15x%2FbtsF1Mnq8sQ%2FpIH3RIz6n3Yd5XKEKMgEk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;357&quot; height=&quot;308&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;308&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;FlowJobConfig (&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/controlling-flow.html&quot;&gt;https://docs.spring.io/spring-batch/reference/step/controlling-flow.html&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Flow 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job은 여러 개의 Step으로 구성됩니다. 각 Step은 특정 작업을 수행합니다.&lt;/li&gt;
&lt;li&gt;start(stepOne)을 사용하여 flowJob이 StepOne으로 시작하도록 정의합니다.&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;.on(&quot;*&quot;).to(stepTwo)는 StepOne이 성공한 경우에만 StepTwo로 이동하도록 설정합니다. 이는 StepOne의 실행 결과가 어떤 것이든 성공한 경우에 StepTwo로 흐름을 이동하게 됨을 의미합니다.&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;.from(stepOne).on(&quot;FAILED&quot;).to(stepThree)은 StepOne이 실패한 경우에만 StepThree으로 이동하도록 설정합니다. 이는 StepOne의 실행 결과가 &quot;FAILED&quot;인 경우에만 StepThree으로 흐름을 이동하게 됨을 의미합니다.&lt;/li&gt;
&lt;li&gt;.on(&quot;FAILED&quot;)은 StepOne의 실행 결과를 지정하는데, 여기서는 &quot;FAILED&quot;를 지정하여 StepOne이 실패한 경우에만 해당합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Flow 종료
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.end()는 Flow 설정을 종료합니다.&lt;br /&gt;이러한 설정을 통해 flowJob은 StepOne으로 시작하여 StepOne의 실행 결과에 따라 StepTwo 또는 StepThree으로 흐름이 이동합니다. StepOne이 성공한 경우에는 StepTwo로 이동하고, StepOne이 실패한 경우에는 StepThree로 이동합니다. 이런 방식으로 Flow를 사용하면 복잡한 작업 흐름을 구성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block; text-align: center;&quot; data-ad-layout=&quot;in-article&quot; data-ad-format=&quot;fluid&quot; data-ad-client=&quot;ca-pub-1657291406799293&quot; data-ad-slot=&quot;6047005770&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemReader, ItemWriter&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ItemReader는 데이터 소스로부터 데이터를 읽어와서 스프링 배치의 처리 과정에 전달하는 역할을 합니다. 다양한 종류의 데이터 소스를 읽을 수 있고, 스프링 배치는 이러한 데이터 소스와 상호 작용할 수 있는 다양한 ItemReader 구현체를 제공합니다.&lt;/h4&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;Chunk 단위로 데이터 읽기: 일정량의 데이터를 한 번에 읽어오는데, 이를 Chunk라고 합니다.&lt;/li&gt;
&lt;li&gt;페이징: 대량의 데이터를 효율적으로 처리하기 위해 페이징 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;Transaction 관리: 데이터 읽기 작업을 트랜잭션 단위로 관리하여 데이터 일관성을 보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ItemWriter는 스프링 배치의 처리 과정에서 생성된 데이터를 지정된 대상에 쓰는 역할을 합니다. 다양한 종류의 대상에 데이터를 쓸 수 있고, 스프링 배치는 이러한 대상과 상호 작용할 수 있는 다양한 ItemWriter 구현체를 제공합니다.&lt;/h4&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;Chunk 단위로 데이터 쓰기: 일정량의 데이터를 한 번에 씁니다.&lt;/li&gt;
&lt;li&gt;Transaction 관리: 데이터 쓰기 작업을 트랜잭션 단위로 관리하여 데이터 일관성을 보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Database Reader Writer 종류&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JpaPagingItemReader, HibernateCursorItemReader(Deprecated), StoredProcedureItemReader, JdbcCursorItemReader, JdbcPagingItemReader&lt;/li&gt;
&lt;li&gt;JpaItemWriter, HibernateItemWriter(Deprecated), JdbcBatchItemWriter&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JdbcJobConfig (&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/readers-and-writers/database.html&quot;&gt;https://docs.spring.io/spring-batch/reference/readers-and-writers/database.html&lt;/a&gt;)&lt;/h4&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class JdbcJobConfig {
    private final DataSource dataSource;

    private static final int chunkSize = 10;

    @Bean
    public Job jdbcJob(JobRepository jobRepository, PlatformTransactionManager transactionManager, Step jdbcStep) {
        return new JobBuilder(&quot;jdbcJob&quot;, jobRepository)
                .start(jdbcStep)
                .build();
    }

    @Bean
    @JobScope
    public Step jdbcStep(@Value(&quot;#{jobParameters[reqDt]}&quot;) String requestDate, JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&quot;jdbcStep&quot;, jobRepository)
                .&amp;lt;BatchDto,BatchDto&amp;gt;chunk(chunkSize,transactionManager)
                .reader(jdbcItemReader())
                .writer(jdbcItemWriter())
                .build();
    }

    @Bean
    @StepScope
    public JdbcCursorItemReader&amp;lt;BatchDto&amp;gt; jdbcItemReader() {
        return new JdbcCursorItemReaderBuilder&amp;lt;BatchDto&amp;gt;()
                .fetchSize(chunkSize)
                .dataSource(dataSource)
                .rowMapper(new BeanPropertyRowMapper&amp;lt;&amp;gt;(BatchDto.class))
                .sql(&quot;SELECT id, name FROM BATCH_TEST&quot;)
                .name(&quot;jdbcItemReader&quot;)
                .build();
    }

    @Bean
    public JdbcBatchItemWriter&amp;lt;BatchDto&amp;gt; jdbcItemWriter() {
        String sql = &quot;UPDATE BATCH_TEST set name = ? where id = ?&quot;;
        return new JdbcBatchItemWriterBuilder&amp;lt;BatchDto&amp;gt;().dataSource(dataSource)
                .sql(sql)
                .itemPreparedStatementSetter((item, ps) -&amp;gt; {
                    ps.setString(1, &quot;SYSTEM&quot;);
                    ps.setLong(2, item.getId());
                })
                .assertUpdates(true)
                .build();
    }

//    @Bean
//    public JdbcBatchItemWriter&amp;lt;BatchDto&amp;gt; jdbcItemWriter() {
//        String sql = &quot;UPDATE BATCH_TEST set name = 'SYSTEM' where id = :id&quot;;
//        return new JdbcBatchItemWriterBuilder&amp;lt;BatchDto&amp;gt;().dataSource(dataSource)
//                .sql(sql)
//                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider&amp;lt;&amp;gt;())
//                .assertUpdates(true)
//                .build();
//    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccMOPi/btsF1AndHJ5/KzDzpdSEJEWWSgTeluPCmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccMOPi/btsF1AndHJ5/KzDzpdSEJEWWSgTeluPCmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccMOPi/btsF1AndHJ5/KzDzpdSEJEWWSgTeluPCmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccMOPi%2FbtsF1AndHJ5%2FKzDzpdSEJEWWSgTeluPCmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;512&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;512&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;ItemReader 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jdbcItemReader 메서드는 JDBC를 사용하여 데이터를 읽는 JdbcCursorItemReader를 생성합니다.&lt;/li&gt;
&lt;li&gt;SQL 쿼리를 사용하여 데이터를 조회하고, fetchSize를 통해 한 번에 가져올 데이터의 양을 설정합니다.&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;jdbcItemWriter 메서드는 JDBC를 사용하여 데이터를 쓰는 JdbcBatchItemWriter를 생성합니다.&lt;/li&gt;
&lt;li&gt;SQL 쿼리를 사용하여 데이터를 업데이트하고, itemPreparedStatementSetter를 통해 PreparedStatement에 값을 설정합니다.&lt;br /&gt;이러한 설정을 통해 스프링 배치를 사용하여 JDBC를 통해 데이터를 읽고 쓰는 배치 작업을 구성할 수 있습니다. 이러한 작업은 대량의 데이터를 처리하거나 주기적으로 반복되는 작업을 수행하는 데 유용합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CompositeJobConfig (&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/readers-and-writers/delegate-pattern-registering.html&quot;&gt;https://docs.spring.io/spring-batch/reference/readers-and-writers/delegate-pattern-registering.html&lt;/a&gt;)&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class CompositeJobConfig {
    private final DataSource dataSource;

    private static final int chunkSize = 10;

    @Bean
    public Job compositeJob(JobRepository jobRepository, PlatformTransactionManager transactionManager, Step compositeStep) {
        return new JobBuilder(&quot;compositeJob&quot;, jobRepository)
                .start(compositeStep)
                .build();
    }

    @Bean
    @JobScope
    public Step compositeStep(@Value(&quot;#{jobParameters[reqDt]}&quot;) String requestDate, JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&quot;compositeStep&quot;, jobRepository)
                .&amp;lt;MemberDto,MemberDto&amp;gt;chunk(chunkSize,transactionManager)
                .reader(compositeItemReader())
                .writer(compositeWriter())
                .build();
    }

    @Bean
    @StepScope
    public JdbcCursorItemReader&amp;lt;MemberDto&amp;gt; compositeItemReader() {
        return new JdbcCursorItemReaderBuilder&amp;lt;MemberDto&amp;gt;()
                .fetchSize(chunkSize)
                .dataSource(dataSource)
                .rowMapper(new BeanPropertyRowMapper&amp;lt;&amp;gt;(MemberDto.class))
                .sql(&quot;SELECT id, name, grade, mileage FROM MEMBER&quot;)
                .name(&quot;jdbcItemReader&quot;)
                .build();
    }

    @Bean
    @StepScope
    public CompositeItemWriter&amp;lt;MemberDto&amp;gt; compositeWriter() {
        final CompositeItemWriter&amp;lt;MemberDto&amp;gt; compositeItemWriter = new CompositeItemWriter&amp;lt;&amp;gt;();
        compositeItemWriter.setDelegates(Arrays.asList(updateGrade(),insertVIP()));
        return compositeItemWriter;
    }

    private CustomBatchItemWriter updateGrade() {
        String sql = &quot;update MEMBER set grade = :grade where id = :id&quot;;

        JdbcBatchItemWriter&amp;lt;MemberDto&amp;gt; itemWriter = new JdbcBatchItemWriterBuilder&amp;lt;MemberDto&amp;gt;()
                .dataSource(dataSource)
                .sql(sql)
                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider&amp;lt;&amp;gt;())
                .build();

        return new CustomBatchItemWriter(itemWriter);
    }

    private ItemWriter&amp;lt;MemberDto&amp;gt; insertVIP() {
        String sql = &quot;insert into VIP(id,name) values(:id, :name)&quot;;
        return items -&amp;gt; {
            JdbcBatchItemWriter&amp;lt;MemberDto&amp;gt; itemWriter = new JdbcBatchItemWriterBuilder&amp;lt;MemberDto&amp;gt;()
                    .dataSource(dataSource)
                    .sql(sql)
                    .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider&amp;lt;&amp;gt;())
                    .build();

            itemWriter.afterPropertiesSet();
            itemWriter.write(new Chunk&amp;lt;MemberDto&amp;gt;(items.getItems().stream()
                    .filter(item -&amp;gt; item.getMileage() &amp;gt; 10000)
                    .collect(Collectors.toList()))
            );
        };
    }
}&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;CompositeItemWriter는 여러 개의 ItemWriter를 단일한 묶음으로 결합합니다.&lt;/li&gt;
&lt;li&gt;하나의 ItemWriter 대신 여러 개의 ItemWriter를 실행하고자 할 때 사용됩니다.&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;여러 개의 ItemWriter를 CompositeItemWriter에 결합하여 복잡한 로직을 분리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;각각의 ItemWriter는 서로 다른 로직 또는 데이터 저장소에 데이터를 쓰는 역할을 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CustomBatchItemWriter (&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/readers-and-writers/custom.html&quot;&gt;https://docs.spring.io/spring-batch/reference/readers-and-writers/custom.html&lt;/a&gt;)&lt;/h4&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class CustomBatchItemWriter implements ItemWriter&amp;lt;MemberDto&amp;gt; {

    private final JdbcBatchItemWriter&amp;lt;MemberDto&amp;gt; jdbcBatchItemWriter;

    @Override
    public void write(Chunk&amp;lt;? extends MemberDto&amp;gt; items) throws Exception {
        for (MemberDto memberDto : items.getItems()) {
            if (memberDto.getMileage() &amp;gt; 10000) {
                memberDto.setGrade(&quot;VIP&quot;);
            }
        }

        jdbcBatchItemWriter.afterPropertiesSet();
        jdbcBatchItemWriter.write(items);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-batch/tree/main/spring-batch-samples&quot;&gt;https://github.com/spring-projects/spring-batch/tree/main/spring-batch-samples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/spring-batch-intro.html&quot;&gt;https://docs.spring.io/spring-batch/reference/spring-batch-intro.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>CompositeItemWriter</category>
      <category>custom Item writer</category>
      <category>JdbcBatchItem</category>
      <category>SpringBatch</category>
      <category>SpringBatch 5.1.1</category>
      <category>스프링배치</category>
      <author>하르싼</author>
      <guid isPermaLink="true">https://devhj.tistory.com/66</guid>
      <comments>https://devhj.tistory.com/66#entry66comment</comments>
      <pubDate>Sun, 24 Mar 2024 21:40:35 +0900</pubDate>
    </item>
  </channel>
</rss>