{"componentChunkName":"component---src-templates-post-template-jsx","path":"/works/posts/2024-06-22--001","result":{"data":{"site":{"siteMetadata":{"title":"Blog by Eunyoung","subtitle":"작업 기록 블로그","copyright":"© All rights reserved.","author":{"name":"EunYoung","twitter":"#"},"disqusShortname":"","url":"https://ssongey.github.io"}},"markdownRemark":{"id":"cb246fff-c515-5282-92be-88c2a25a2465","html":"<p>스프링 배치를 사용하여 매일 한번씩 카카오 계정 서버에서 휴면/탈퇴 계정 목록을 가져와 내가 담당하고 있는 서비스를 탈퇴 시키는 작업을 진행했고, 이에 대한 기록을 남긴다.</p>\n<h2>1. 요구사항</h2>\n<ul>\n<li>매일 휴면계정 목록을 가져와서 카카오 전자증명서 서비스를 탈퇴시킨다.</li>\n<li>매일 탈퇴계정 가져와서 카카오 전자증명서 서비스 탈퇴 및 DB에 저장된 증명서 발급 내역 테이블에서 개인정보 컬럼을 null로 업데이트한다.</li>\n</ul>\n<br/>\n<h2>2. 배치 스케줄러를 어떻게 수행할 것인가?</h2>\n<h3>2.1. k8s cronJob + api 서버</h3>\n<p><strong>방법</strong></p>\n<ul>\n<li>스케줄러는 k8s cronJob 이 담당</li>\n<li>배치 작업을 수행하는 api 를 생성하며, cronJob이 해당 api를 호출한다.</li>\n<li>해당 api 의 배치 로직 실패시에 알람 구현 필요</li>\n</ul>\n<p><strong>배치 오류 발생시?</strong></p>\n<ul>\n<li>상세 에러 내용은 kibana에서 확인 (es에 서빙이 되도록 로그 작업 필요)</li>\n<li>배치 작업을 수행하는 api 를 수동으로 호출 필요</li>\n</ul>\n<br/>\n<h3>2.2. jenkins + api 서버</h3>\n<p><strong>방법</strong></p>\n<ul>\n<li>스케줄러는 jenkins cron이 담당</li>\n<li>배치 작업을 수행하는 api 를 생성하며, jenkins가 해당 api를 호출한다.</li>\n<li>실패에 대한 알람은 jenkins 플러그인 사용</li>\n</ul>\n<p><strong>배치 오류 발생시?</strong></p>\n<ul>\n<li>\n<p>상세 에러 내용은 kibana에서 확인 (es에 서빙이 되도록 로그 작업 필요)</p>\n<ul>\n<li>배치가 오래 걸릴수 있으니 http 커넥션을 계속 맺고 있을수 없다고 판단, @Async 어노테이션을 이용하여 비동기로 수행. 때문에 젠킨스에 배치에 대한 호출로그는 남겠지만, 수행 로그는 남지 않음.</li>\n</ul>\n</li>\n<li>수동으로 실행하는 젠킨스 파이프라인 미리 생성하여 해당 파이프라인 수행.</li>\n</ul>\n<br/>\n<h3>2.3. jenkins + springbatch</h3>\n<p><strong>방법</strong></p>\n<ul>\n<li>스케줄러는 jenkins cron이 담당</li>\n<li>배치 작업은 spring batch가 담당, jenkins cron은 springbatch 애플리케이션 파드를 실행시킴</li>\n<li>수행 시간에 파드가 생겼다가 작업이 끝나면 사라짐.</li>\n<li>실패에 대한 알람은 jenkins 플러그인 사용</li>\n</ul>\n<p><strong>배치 오류 발생시?</strong></p>\n<ul>\n<li>상세 에러 내용은 젠킨스 수행 로그에서 확인</li>\n<li>수동으로 실행하는 젠킨스 파이프라인 미리 생성하여 해당 파이프라인 수행.</li>\n</ul>\n<p><strong>3번 사용 결정 이유</strong></p>\n<ul>\n<li>\n<p>배치를 위한 파드(서버)를 계속 띄울 필요 없이, 배치가 수행되는 시간에만 파드를 띄울수 있다는 장점이 있다.</p>\n<ul>\n<li>리소스 절약 및 서버에 대한 관리가 불필요.</li>\n<li>\n<p>파트 내에 이미 kubernetes plugin 이 설치된 젠킨스가 있어서 세팅이 간단 했던것도 한몫함.</p>\n<ul>\n<li><a href=\"https://plugins.jenkins.io/kubernetes\">Jenkins kubernetes plugin</a></li>\n</ul>\n</li>\n</ul>\n</li>\n<li>\n<p>한 곳에서 (jenkins) 수행 로그(결과 또는 에러 로그) 확인 및 재수행(retry)을 진행할 수 있다는 장점이 있다.</p>\n<ul>\n<li>api로 수행을 할 경우, 수행 로그 확인과 재수행(retry) 위치가 달라진다.</li>\n</ul>\n</li>\n</ul>\n<br/>\n<h2>3. spring batch 작업을 진행하면서 겪은 일</h2>\n<h3>3.1. multi datasource 설정</h3>\n<ul>\n<li>spring batch 를 사용하기 위해서는 <strong>기본적으로 필요한 메타 테이블들이 존재</strong>한다.</li>\n<li>해당 테이블은 비즈니스 로직에서 사용하는 DB에 생성하기에는 너무 불필요한 테이블이였다.</li>\n<li>\n<p>때문에 <strong>비즈니스 DB와 spring batch DB를 분리</strong>하기로 결정.</p>\n<ul>\n<li><strong>비즈니스 DB는 기존대로 mysql 사용, spring batch DB는 h2 사용.</strong></li>\n<li>파드가 생성되어 애플리케이션이 시작될때 h2 로드 및 아래 spring batch 메타 테이블은 h2에 생성.</li>\n<li>애플리케이션이 종료되면 h2에 생성된 테이블 삭제 및 h2도 같이 종료. (메타 테이블에 저장된 배치 이력 데이터 사라짐)</li>\n</ul>\n</li>\n</ul>\n<p>\n  <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/58a91/01.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n  \n  <span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block;  max-width: 960px; margin-left: auto; margin-right: auto;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 80.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAAClElEQVQ4y4VUaXPaMBTk//+cNue0nU4DTSbhDtjGFz4xYPAFtqmxzfZJQJoPIdXMWpZk7Vvte3LDj1Lcv0i4ex7j9umV4+5ZQEdxcakdDoeLa41NusVYmkCcyLwXJBlBvMGu2GObF3BXMWYEexFgHiSoT2SXSBtRskFvJHK0ByOMJBWiOoVhu/BWIa4eOrj69YLrZge/XxWUVf05YV3XqOoD2HKa5fRe8037skK+y+HYJhzTgL/wgLp6I7pIyB6iscCXZh83j0N8bfbwrS1ht68Qxgl64wk6wzGpH0Mi5fuy5BuZkJqEvAcLwgkX6xUE8lCSFTgzD0GUoOIqa6RFhWibI87+ICtKTvRZ44RRFNHRbNi2BcdxkGUZXzQpEdePr7hu9fH1oYvvbREFWTEPUnSpCgbaDH3VRVe20SFE6e5IuA5D6NMpFEVFSO9nf7abmOYUqATHsXlg1py5j2Z7iObLAK3OEK+KAc1ZYpOdCYMAuq7zzZZloSSf9vs9wijm85qmwTRNFEXBPQyI2KXT2JQslrRzqylpnHC1XmNKCjVV5ZvZRkYaJQkMw+CBPM/j/rFA7EQsgKpq8H3/jZBVyD+FROiSfyqRuu7xlmzTFBapMGhtSkp9f8nnfRIgSRIH8zzZbBAQR57np6TEMUyLJcTlkZmPrAVhBEHWMBJlKngZE1VHSQrZTVLtOXRnTr0H1ZphYhDxNj0ShuTJ9OQVQ0zjkrK53uywTHLYPnm2TuCFWyRUPv8tG8dboi/IGBBYLyo65usIP9oC7p+GuGl1cdvq4WdvgpEx51VwCZyQihzVO7Axu3ayQj8NQYAkitwvppz5dC7uj65f4zj5DqePsnwHQTMxVg0OaWrT2IJuz05q8KHCv7datrAKw+zRAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n    >\n      <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width: 100%; height: 100%; margin: 0; vertical-align: middle; position: absolute; top: 0; left: 0; box-shadow: inset 0px 0px 0px 400px white;\"\n        alt=\"01\"\n        title=\"\"\n        src=\"/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/d9199/01.png\"\n        srcset=\"/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/8ff5a/01.png 240w,\n/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/e85cb/01.png 480w,\n/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/d9199/01.png 960w,\n/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/07a9c/01.png 1440w,\n/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/29114/01.png 1920w,\n/devHistoryBlog/static/bce78d74ec50ca823267f3786e435b30/58a91/01.png 1990w\"\n        sizes=\"(max-width: 960px) 100vw, 960px\"\n      />\n    </span>\n  </span>\n  \n  </a>\n    </p>\n<ul>\n<li>비즈니스 로직에서 사용하는 DB(mysql) 설정(application.yaml)</li>\n</ul>\n<div class=\"gatsby-highlight\" data-language=\"kotlin\"><pre class=\"language-kotlin\"><code class=\"language-kotlin\">spring<span class=\"token operator\">:</span>\n  jpa<span class=\"token operator\">:</span>\n    database<span class=\"token operator\">:</span> mysql\n    database<span class=\"token operator\">-</span>platform<span class=\"token operator\">:</span> org<span class=\"token punctuation\">.</span>hibernate<span class=\"token punctuation\">.</span>dialect<span class=\"token punctuation\">.</span>MySQL8Dialect\n    <span class=\"token operator\">..</span><span class=\"token punctuation\">.</span></code></pre></div>\n<ul>\n<li>spring batch에서 필요한 메타 데이터 테이블 생성을 위한 설정 (BatchConfig.kt)</li>\n</ul>\n<div class=\"gatsby-highlight\" data-language=\"kotlin\"><pre class=\"language-kotlin\"><code class=\"language-kotlin\"><span class=\"token annotation builtin\">@EnableConfigurationProperties</span><span class=\"token punctuation\">(</span>BatchProperties<span class=\"token operator\">::</span><span class=\"token keyword\">class</span><span class=\"token punctuation\">)</span>\n<span class=\"token annotation builtin\">@Configuration</span>\n<span class=\"token keyword\">class</span> BatchConfig <span class=\"token punctuation\">{</span>\n\t\t<span class=\"token comment\">// @Bean 없이 생성해야 primany datasource bean 은 mysql 이 된다.</span>\n    <span class=\"token keyword\">private</span> <span class=\"token keyword\">val</span> batchDataSource <span class=\"token operator\">=</span>\n        <span class=\"token function\">EmbeddedDatabaseBuilder</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">setType</span><span class=\"token punctuation\">(</span>EmbeddedDatabaseType<span class=\"token punctuation\">.</span>H2<span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">addScript</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"classpath:org/springframework/batch/core/schema-drop-h2.sql\"</span><span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">addScript</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"classpath:/org/springframework/batch/core/schema-h2.sql\"</span><span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">build</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n\n\t\t<span class=\"token comment\">// 메타 테이블 스키마에 대한 초기화를 진행한다. 해당 설정이 없으면 테이블 생성은 mysql에 생성된다.</span>\n    <span class=\"token annotation builtin\">@Bean</span>\n    <span class=\"token keyword\">fun</span> <span class=\"token function\">batchDataSourceInitializer</span><span class=\"token punctuation\">(</span>\n        resourceLoader<span class=\"token operator\">:</span> ResourceLoader<span class=\"token operator\">?</span><span class=\"token punctuation\">,</span>\n        properties<span class=\"token operator\">:</span> BatchProperties<span class=\"token operator\">?</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> BatchDataSourceInitializer <span class=\"token operator\">=</span>\n        <span class=\"token function\">BatchDataSourceInitializer</span><span class=\"token punctuation\">(</span>\n            batchDataSource<span class=\"token punctuation\">,</span>\n            resourceLoader<span class=\"token punctuation\">,</span>\n            properties<span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">)</span>\n\n\t\t<span class=\"token comment\">// 배치설정를 진행하며, 여기서 설정된 datasource에 배치 메타 데이터에 대해 crud 가 발생</span>\n    <span class=\"token annotation builtin\">@Bean</span>\n    <span class=\"token keyword\">fun</span> <span class=\"token function\">batchConfigurer</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> BatchConfigurer <span class=\"token operator\">=</span> <span class=\"token function\">DefaultBatchConfigurer</span><span class=\"token punctuation\">(</span>batchDataSource<span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<br/>\n<h3>3.2. JPA로 수행되는 CUD 쿼리가 수행이 안되는 현상.</h3>\n<ul>\n<li>위처럼 생성하게 되면 batch 관련 DataSource 는 h2 기준으로 생성이 된다.</li>\n<li>Spring Batch는 기본적으로 <code class=\"language-text\">DataSourceTransactionManager</code> 를 사용하므로 JPA로 수행되는 쿼리도 해당 트랜잭션 매니저로 수행이 된다.</li>\n<li><code class=\"language-text\">DataSourceTransactionManager</code>으로 수행이 되면 JpaTransaction이 생성되지 않아 기본 쿼리인 조회(select)만 수행이 가능하게 된다.</li>\n<li>이를 해결하기 위해 JpaTransaction bean을 생성하여 batch job에 주입해준다.</li>\n</ul>\n<div class=\"gatsby-highlight\" data-language=\"kotlin\"><pre class=\"language-kotlin\"><code class=\"language-kotlin\"><span class=\"token annotation builtin\">@Configuration</span>\n<span class=\"token keyword\">class</span> JpaConfig <span class=\"token punctuation\">{</span>\n\t\t<span class=\"token comment\">// datasource bean 은 mysql 이다.</span>\n    <span class=\"token annotation builtin\">@Bean</span>\n    <span class=\"token annotation builtin\">@Primary</span>\n    <span class=\"token keyword\">fun</span> <span class=\"token function\">jpaTransactionManager</span><span class=\"token punctuation\">(</span>dataSource<span class=\"token operator\">:</span> DataSource<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> JpaTransactionManager <span class=\"token punctuation\">{</span>\n        <span class=\"token keyword\">return</span> <span class=\"token function\">JpaTransactionManager</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">apply</span> <span class=\"token punctuation\">{</span> <span class=\"token keyword\">this</span><span class=\"token punctuation\">.</span>dataSource <span class=\"token operator\">=</span> dataSource <span class=\"token punctuation\">}</span>\n    <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">class</span> JobConfig <span class=\"token punctuation\">{</span>\n\t\t<span class=\"token annotation builtin\">@Bean</span>\n    <span class=\"token keyword\">fun</span> <span class=\"token function\">dormantStep</span><span class=\"token punctuation\">(</span>\n        jpaTransactionManager<span class=\"token operator\">:</span> JpaTransactionManager<span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> Step <span class=\"token punctuation\">{</span>\n        <span class=\"token keyword\">return</span> stepBuilderFactory<span class=\"token punctuation\">[</span><span class=\"token string\">\"dormantStep\"</span><span class=\"token punctuation\">]</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">tasklet</span><span class=\"token punctuation\">(</span>dormantUserTasklet<span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">transactionManager</span><span class=\"token punctuation\">(</span>jpaTransactionManager<span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">build</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">}</span>\n\n    <span class=\"token annotation builtin\">@Bean</span>\n    <span class=\"token keyword\">fun</span> <span class=\"token function\">dormantJob</span><span class=\"token punctuation\">(</span>\n        dormantStep<span class=\"token operator\">:</span> Step<span class=\"token punctuation\">,</span>\n        jobCompletionListener<span class=\"token operator\">:</span> JobExecutionListener\n    <span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> Job <span class=\"token punctuation\">{</span>\n        <span class=\"token keyword\">return</span> jobBuilderFactory<span class=\"token punctuation\">[</span><span class=\"token string\">\"dormantJob\"</span><span class=\"token punctuation\">]</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">listener</span><span class=\"token punctuation\">(</span>jobCompletionListener<span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">start</span><span class=\"token punctuation\">(</span>dormantStep<span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">.</span><span class=\"token function\">build</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<br/>\n<h3>3.3. index가 없을 경우 update, delete 쿼리가 수행되면?</h3>\n<ul>\n<li>update, delete 쿼리에 필요한 인덱스를 DB팀에 요청 후에 jenkins 세팅을 하게 되었다.</li>\n<li>리얼에서 파이프라인 세팅 및 테스트를 하다가 실수로 spring batch job을 수행하게 되었다.</li>\n<li>갑자기 약 1-2분간 ‘PessimisticLockingFailureException’ 오류가 발생하여 서비스 불능 상태가 되었고, batch job 수행이 완료된 후 락이 풀렸다.</li>\n<li>\n<p>where 절로 인해 테이블 전체가 Lock이 걸리진 않을거라고 예상했는데… <strong>InnoDB에서 <a href=\"https://dev.mysql.com/doc/refman/8.4/en/update.html\"><code class=\"language-text\">UPDATE</code></a>또는 <a href=\"https://dev.mysql.com/doc/refman/8.4/en/delete.html\"><code class=\"language-text\">DELETE</code></a>는 where 절과 상관없이 스캔된 모든 인덱스 레코드에 잠금을 건다</strong>. 수행 당시에는 조건에 걸린 필드에 인덱스가 없었기 때문에 모든 row에 락이 걸리게 된 것이다.</p>\n<ul>\n<li><a href=\"https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html\">https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html</a></li>\n</ul>\n</li>\n<li>실시간 서비스에서 서치가 오래걸리는 update, delete 의 경우는 꼭꼭 인덱스를 세팅하고 수행하자.</li>\n</ul>\n<br/>\n<h3>4. Transaction 이 너무 긴 현상</h3>\n<div class=\"gatsby-highlight\" data-language=\"kotlin\"><pre class=\"language-kotlin\"><code class=\"language-kotlin\"><span class=\"token keyword\">class</span> <span class=\"token function\">ATestlet</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> Tasklet <span class=\"token punctuation\">{</span>\n\t<span class=\"token keyword\">override</span> <span class=\"token keyword\">fun</span> <span class=\"token function\">execute</span><span class=\"token punctuation\">(</span> <span class=\"token punctuation\">{</span>\n\t\t<span class=\"token operator\">..</span><span class=\"token punctuation\">.</span>\n\t\t<span class=\"token function\">loop</span> <span class=\"token punctuation\">(</span><span class=\"token number\">100</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n\t\t\t<span class=\"token function\">run</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n\t\t<span class=\"token punctuation\">}</span>\n\t<span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">class</span> <span class=\"token function\">RunnerClass</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n\t<span class=\"token annotation builtin\">@Transactional</span>\n\t<span class=\"token keyword\">fun</span> <span class=\"token function\">run</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n\t\t<span class=\"token operator\">..</span><span class=\"token punctuation\">.</span>\n\t<span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<ul>\n<li>한개의 tasklet 을 사용하여 loop를 돌며 트랜젝션 어노테이션이 붙은 함수를 수행하도록 하였고, 해당 함수에서는 update, delete 쿼리가 수행된다.</li>\n<li>loop 가 한번 돌떄마다 commit 이 발생할 것이라고 예상했는데, 예상한바와 다르게 프로세스가 끝나야 실제 DB에 반영이 되었다.</li>\n<li>해당 원인파악은 아직 못한 상태…</li>\n</ul>","fields":{"tagSlugs":["/tags/spring-batch/"],"slug":"/works/posts/2024-06-22--001"},"frontmatter":{"title":"스프링 배치작업 기록","tags":["springBatch"],"date":"2024-06-22","description":""}}},"pageContext":{"slug":"/works/posts/2024-06-22--001"}},"staticQueryHashes":[]}