<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기록의 탑</title>
    <link>https://j0free.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 12 Apr 2026 10:28:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>j0free</managingEditor>
    <item>
      <title>10. 스프링배치 플로우 컨트롤 하기</title>
      <link>https://j0free.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 데이터를 읽고 저장하는 순서대로 처리하는 배치 작업에 대해 알아봤다. 이번에는 배치 작업에서 여러 step을 정의하고 조건에 따라 순서대로 실행하거나 특정 step을 건너 뛸 수 있는 Spring Batch의 Flow Controller에 대해 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Flow 컨트롤 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;next: 현재 Step이 성공적으로 종료되면 다음 Step으로 이동한다.&lt;/li&gt;
&lt;li&gt;from: 특정 Step에서 현재 Step으로 이동한다.&lt;/li&gt;
&lt;li&gt;on: 특정 ExitStatus에 따라 다음 Step을 결정한다.&lt;/li&gt;
&lt;li&gt;to: 특정 Step으로 이동한다.&lt;/li&gt;
&lt;li&gt;stop: 현재 Flow를 종료한다.&lt;/li&gt;
&lt;li&gt;end: FlowBuilder를 종료한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Flow 컨트롤&lt;/h2&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;next&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;next는 Start 스텝이 수행하고 난 뒤, next 스텝으로 이동하게 된다.&lt;/li&gt;
&lt;li&gt;next는 계속해서 추가 될 수 있으며, start --&amp;gt; next --&amp;gt; next ... 순으로 진행되도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1733819818617&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class NextStepTaskJobConfiguration {

	public static final String NEXT_STEP_TASK = &quot;NEXT_STEP_TASK&quot;;

	private final PlatformTransactionManager transactionManager;

	@Bean(name = &quot;step01&quot;)
	public Step step01(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;step01&quot;, jobRepository)
			.tasklet((contribution, chunkContext) -&amp;gt; {
				log.info(&quot;Execute Step 01 Tasklet ...&quot;);
				return RepeatStatus.FINISHED;
			}, transactionManager)
			.build();
	}

	@Bean(name = &quot;step02&quot;)
	public Step step02(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;step02&quot;, jobRepository)
			.tasklet((contribution, chunkContext) -&amp;gt; {
				log.info(&quot;Execute Step 02 Tasklet ...&quot;);
				return RepeatStatus.FINISHED;
			}, transactionManager)
			.build();
	}

	@Bean
	public Job nextStepJob(@Qualifier(&quot;step01&quot;) Step step01, @Qualifier(&quot;step02&quot;) Step step02, JobRepository jobRepository) {
		log.info(&quot;------------------ Init myJob -----------------&quot;);
		return new JobBuilder(NEXT_STEP_TASK, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(step01)
			.next(step02)
			.build();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;on&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;on 은 특정 스텝의 종료 조건에 따라 어떠한 스텝으로 이동할지 결정할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;아래 예는 Step01을 먼저 수행하고, 해당 결과에 따라서 다음 스텝으로 이동하는 플로우를 보여준다.
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;on(&quot;FAILED&quot;) 인경우 step03을 수행하도록 한다.&lt;/li&gt;
&lt;li&gt;from(step01).on(&quot;COMPLETED&quot;) 인경우 step01의 결과 완료인경우라면 step02를 수행하도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이처럼 on과 from을 통해서 스텝의 종료 조건에 따라 원하는 플로우를 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1733819846339&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class OnStepTaskJobConfiguration {

	public static final String ON_STEP_TASK = &quot;ON_STEP_TASK&quot;;

	private final PlatformTransactionManager transactionManager;

	@Bean(name = &quot;stepOn01&quot;)
	public Step stepOn01(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;stepOn01&quot;, jobRepository)
			.tasklet((contribution, chunkContext) -&amp;gt; {
				log.info(&quot;Execute Step 01 Tasklet ...&quot;);

				Random random = new Random();
				int randomValue = random.nextInt(1000);

				if (randomValue % 2 == 0) {
					return RepeatStatus.FINISHED;
				} else {
					throw new RuntimeException(&quot;Error This value is Odd: &quot; + randomValue);
				}
			}, transactionManager)
			.build();
	}

	@Bean(name = &quot;stepOn02&quot;)
	public Step stepOn02(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;stepOn02&quot;, jobRepository)
			.tasklet(new Tasklet() {
				@Override
				public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
					log.info(&quot;Execute Step 02 Tasklet ...&quot;);
					return RepeatStatus.FINISHED;
				}
			}, transactionManager)
			.build();
	}

	@Bean(name = &quot;stepOn03&quot;)
	public Step stepOn03(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;stepOn03&quot;, jobRepository)
			.tasklet((contribution, chunkContext) -&amp;gt; {
				log.info(&quot;Execute Step 03 Tasklet ...&quot;);
				return RepeatStatus.FINISHED;
			}, transactionManager)
			.build();
	}

	@Bean
	public Job onStepJob(@Qualifier(&quot;stepOn01&quot;) Step stepOn01, @Qualifier(&quot;stepOn02&quot;) Step stepOn02, @Qualifier(&quot;stepOn03&quot;) Step stepOn03, JobRepository jobRepository) {
		log.info(&quot;------------------ Init myJob -----------------&quot;);
		return new JobBuilder(ON_STEP_TASK, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(stepOn01)
			.on(&quot;FAILED&quot;).to(stepOn03)
			.from(stepOn01).on(&quot;COMPLETED&quot;).to(stepOn02)
			.end()
			.build();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;stop&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stop은 특정 step의 작업 결과의 상태를 보고 정지할지 결정한다.&lt;/li&gt;
&lt;li&gt;아래 예제는 step1의 결과가 실패인경우 stop을 통해 배치 작업을 정지하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1733819872447&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class StopStepTaskJobConfiguration {

	public static final String STOP_STEP_TASK = &quot;STOP_STEP_TASK&quot;;

	private final PlatformTransactionManager transactionManager;

	@Bean(name = &quot;stepStop01&quot;)
	public Step stepStop01(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;stepStop01&quot;, jobRepository)
			.tasklet(new Tasklet() {
				@Override
				public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
					log.info(&quot;Execute Step 01 Tasklet ...&quot;);

					Random random = new Random();
					int randomValue = random.nextInt(1000);

					if (randomValue % 2 == 0) {
						return RepeatStatus.FINISHED;
					} else {
						throw new RuntimeException(&quot;Error This value is Odd: &quot; + randomValue);
					}
				}
			}, transactionManager)
			.build();
	}

	@Bean(name = &quot;stepStop02&quot;)
	public Step stepStop02(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;stepStop02&quot;, jobRepository)
			.tasklet(new Tasklet() {
				@Override
				public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
					log.info(&quot;Execute Step 02 Tasklet ...&quot;);
					return RepeatStatus.FINISHED;
				}
			}, transactionManager)
			.build();
	}

	@Bean
	public Job stopStepJob(@Qualifier(&quot;stepStop01&quot;) Step stepOn01, @Qualifier(&quot;stepStop02&quot;) Step stepOn02, JobRepository jobRepository) {
		log.info(&quot;------------------ Init myJob -----------------&quot;);
		return new JobBuilder(STOP_STEP_TASK, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(stepOn01)
			.on(&quot;FAILED&quot;).stop()
			.from(stepOn01).on(&quot;COMPLETED&quot;).to(stepOn02)
			.end()
			.build();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flow Controller를 이용하면 Job의 여러 step을 조건에 따라 수행 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/16</guid>
      <comments>https://j0free.tistory.com/16#entry16comment</comments>
      <pubDate>Tue, 10 Dec 2024 17:38:41 +0900</pubDate>
    </item>
    <item>
      <title>9. 입맛에 맞는 배치 처리를 위한 Custom ItemReader/ItemWriter 구현</title>
      <link>https://j0free.tistory.com/15</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;이전 글에서는 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;ItemReader와 ItemWriter를 구현하는 방법에 대해 다뤘습니다. 기본 제공되는 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;ItemReader와 ItemWriter로 충분하지 않거나 복합한 쿼리를 수행하는 경우, 비지니스 로직에 맞게 Custom &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;ItemReader와 ItemWriter 구현이 필요합니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;이럴 땐, QueryDsl을 이용하여 Custom하게 작성 할 수 있습니다. 허나 문제가 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Spring Batch는 공식적으로 QuerydslItemReader를 지원하지 않습니다. 이를 위해서는 AbstractPagingItemReader를 상속하여 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;QuerydslPagingItemReader를 생성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/appendix.html#listOfReadersAndWriters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Batch ItemReader/ItemWriter 지원 목록&lt;/a&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;QuerydslPagingItemReader&lt;/h2&gt;
&lt;pre id=&quot;code_1733817412901&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class QuerydslPagingItemReader&amp;lt;T&amp;gt; extends AbstractPagingItemReader&amp;lt;T&amp;gt; {

	private EntityManager em;
	private final Function&amp;lt;JPAQueryFactory, JPAQuery&amp;lt;T&amp;gt;&amp;gt; querySupplier;

	private final Boolean alwaysReadFromZero;

	public QuerydslPagingItemReader(EntityManagerFactory entityManagerFactory, Function&amp;lt;JPAQueryFactory, JPAQuery&amp;lt;T&amp;gt;&amp;gt; querySupplier, int chunkSize) {
		this(ClassUtils.getShortName(QuerydslPagingItemReader.class), entityManagerFactory, querySupplier, chunkSize, false);
	}

	public QuerydslPagingItemReader(String name, EntityManagerFactory entityManagerFactory, Function&amp;lt;JPAQueryFactory, JPAQuery&amp;lt;T&amp;gt;&amp;gt; querySupplier, int chunkSize, Boolean alwaysReadFromZero) {
		super.setPageSize(chunkSize);
		setName(name);
		this.querySupplier = querySupplier;
		this.em = entityManagerFactory.createEntityManager();
		this.alwaysReadFromZero = alwaysReadFromZero;

	}

	@Override
	protected void doClose() throws Exception {
		if (em != null)
			em.close();
		super.doClose();
	}

	@Override
	protected void doReadPage() {
		initQueryResult();

		JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(em);
		long offset = 0;
		if (!alwaysReadFromZero) {
			offset = (long) getPage() * getPageSize();
		}

		JPAQuery&amp;lt;T&amp;gt; query = querySupplier.apply(jpaQueryFactory).offset(offset).limit(getPageSize());

		List&amp;lt;T&amp;gt; queryResult = query.fetch();
		for (T entity: queryResult) {
			em.detach(entity);
			results.add(entity);
		}
	}

	private void initQueryResult() {
		if (CollectionUtils.isEmpty(results)) {
			results = new CopyOnWriteArrayList&amp;lt;&amp;gt;();
		} else {
			results.clear();
		}
	}
}&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;alwaysReadFromZero: 항상 0부터 페이징을 읽을지 여부를 지정한다. 만약 paging 처리된 데이터 자체를 수정하는경우 배치처리 누락이 발생할 수 있으므로 이를 해결하기 위한 방안으로 사용된다.&lt;/li&gt;
&lt;li&gt;AbstractPagingItemReader은 어댑터 패턴으로, 상속받는 쪽은 doReadPage만 구현하여 페이징 처리하여 데이터를 가져올 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;doClose는 기본적으로 AbstractPagingItemReader를 자체 구현되어 있지만 EntityManager자원을 해제하기 위해서 em.close() 를 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;QuerydslPagingItemReader를 편하게 작성하기 위해 빌더 코드를 추가 해보자&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1733817592854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class QuerydslPagingItemReaderBuilder&amp;lt;T&amp;gt; {

	private EntityManagerFactory entityManagerFactory;
	private Function&amp;lt;JPAQueryFactory, JPAQuery&amp;lt;T&amp;gt;&amp;gt; querySupplier;

	private int chunkSize = 10;

	private String name;

	private Boolean alwaysReadFromZero;

	public QuerydslPagingItemReaderBuilder&amp;lt;T&amp;gt; entityManagerFactory(EntityManagerFactory entityManagerFactory) {
		this.entityManagerFactory = entityManagerFactory;
		return this;
	}

	public QuerydslPagingItemReaderBuilder&amp;lt;T&amp;gt; querySupplier(Function&amp;lt;JPAQueryFactory, JPAQuery&amp;lt;T&amp;gt;&amp;gt; querySupplier) {
		this.querySupplier = querySupplier;
		return this;
	}

	public QuerydslPagingItemReaderBuilder&amp;lt;T&amp;gt; chunkSize(int chunkSize) {
		this.chunkSize = chunkSize;
		return this;
	}

	public QuerydslPagingItemReaderBuilder&amp;lt;T&amp;gt; name(String name) {
		this.name = name;
		return this;
	}

	public QuerydslPagingItemReaderBuilder&amp;lt;T&amp;gt; alwaysReadFromZero(Boolean alwaysReadFromZero) {
		this.alwaysReadFromZero = alwaysReadFromZero;
		return this;
	}

	public QuerydslPagingItemReader&amp;lt;T&amp;gt; build() {
		if (name == null) {
			this.name = ClassUtils.getShortName(QuerydslPagingItemReader.class);
		}
		if (this.entityManagerFactory == null) {
			throw new IllegalArgumentException(&quot;EntityManagerFactory can not be null.!&quot;);
		}
		if (this.querySupplier == null) {
			throw new IllegalArgumentException(&quot;Function&amp;lt;JPAQueryFactory, JPAQuery&amp;lt;T&amp;gt;&amp;gt; can not be null.!&quot;);
		}
		if (this.alwaysReadFromZero == null) {
			alwaysReadFromZero = false;
		}
		return new QuerydslPagingItemReader&amp;lt;&amp;gt;(this.name, entityManagerFactory, querySupplier, chunkSize, alwaysReadFromZero);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;소스 샘플&lt;/h2&gt;
&lt;pre id=&quot;code_1733817710127&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class QueryDSLPagingReaderJobConfig {

	/**
	 * CHUNK 크기를 지정한다.
	 */
	public static final int CHUNK_SIZE = 2;
	public static final String ENCODING = &quot;UTF-8&quot;;
	public static final String QUERYDSL_PAGING_CHUNK_JOB = &quot;QUERYDSL_PAGING_CHUNK_JOB&quot;;

	private final DataSource dataSource;
	private final EntityManagerFactory entityManagerFactory;

	//    @Bean
	//    public QuerydslPagingItemReader&amp;lt;Customer&amp;gt; customerQuerydslPagingItemReader() throws Exception {
	//
	//        Function&amp;lt;JPAQueryFactory, JPAQuery&amp;lt;Customer&amp;gt;&amp;gt; query = jpaQueryFactory -&amp;gt; jpaQueryFactory.select(QCustomer.customer).from(QCustomer.customer);
	//
	//        return new QuerydslPagingItemReader&amp;lt;&amp;gt;(&quot;customerQuerydslPagingItemReader&quot;, entityManagerFactory, query, CHUNK_SIZE, false);
	//    }


	@Bean
	public QuerydslPagingItemReader&amp;lt;Customer&amp;gt; customerQuerydslPagingItemReader() {
		return new QuerydslPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;customerQuerydslPagingItemReader&quot;)
			.entityManagerFactory(entityManagerFactory)
			.chunkSize(2)
			.querySupplier(jpaQueryFactory -&amp;gt; jpaQueryFactory.select(QCustomer.customer).from(QCustomer.customer).where(QCustomer.customer.age.gt(20)))
			.build();
	}

	@Bean
	public FlatFileItemWriter&amp;lt;Customer&amp;gt; customerQuerydslFlatFileItemWriter() {

		return new FlatFileItemWriterBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;customerQuerydslFlatFileItemWriter&quot;)
			.resource(new FileSystemResource(&quot;./customer_new_v2.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;\t&quot;)
			.names(&quot;Name&quot;, &quot;Age&quot;, &quot;Gender&quot;)
			.build();
	}


	@Bean
	public Step customerQuerydslPagingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) throws Exception {
		log.info(&quot;------------------ Init customerQuerydslPagingStep -----------------&quot;);

		return new StepBuilder(&quot;customerJpaPagingStep&quot;, jobRepository)
			.&amp;lt;Customer, Customer&amp;gt;chunk(CHUNK_SIZE, transactionManager)
			.reader(customerQuerydslPagingItemReader())
			.processor(new CustomerItemProcessor())
			.writer(customerQuerydslFlatFileItemWriter())
			.build();
	}

	@Bean
	public Job customerQueryDslJpaPagingJob(@Qualifier(&quot;customerQuerydslPagingStep&quot;) Step customerJdbcPagingStep, JobRepository jobRepository) {
		log.info(&quot;------------------ Init customerQueryDslJpaPagingJob -----------------&quot;);
		return new JobBuilder(QUERYDSL_PAGING_CHUNK_JOB, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(customerJdbcPagingStep)
			.build();
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CustomItemWriter&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CustomItemWriter는 Spring Batch에서 제공하는 기본 ItemWriter 인터페이스를 구현하여 직접 작성한 ItemWriter 클래스이다.&lt;/li&gt;
&lt;li&gt;기본 ItemWriter 클래스로는 제공되지 않는 특정 기능을 구현할 때 사용된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;구성 요소&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemWriter 인터페이스 구현: write() 메소드를 구현하여 원하는 처리를 수행한다.&lt;/li&gt;
&lt;li&gt;필요한 라이브러리 및 객체 선언: 사용할 라이브러리 및 객체를 선언한다.&lt;/li&gt;
&lt;li&gt;데이터 처리 로직 구현: write() 메소드에서 데이터 처리 로직을 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유연성: 기본 ItemWriter 클래스로는 제공되지 않는 특정 기능을 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;확장성: 다양한 방식으로 데이터 처리를 확장할 수 있다.&lt;/li&gt;
&lt;li&gt;제어 가능성: 데이터 처리 과정을 완벽하게 제어할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 복잡성: 기본 ItemWriter 클래스보다 개발 과정이 더 복잡하다.&lt;/li&gt;
&lt;li&gt;테스트 어려움: 테스트 작성이 더 어려울 수 있다.&lt;/li&gt;
&lt;li&gt;디버깅 어려움: 문제 발생 시 디버깅이 더 어려울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;CustomService&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;청크 아이템을 받아서 호출하는 서비스를 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1733818022836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class CustomItemWriter implements ItemWriter&amp;lt;CustomerDto&amp;gt; {

	private final CustomService customService;

	public CustomItemWriter(CustomService customService) {
		this.customService = customService;
	}

	@Override
	public void write(Chunk&amp;lt;? extends CustomerDto&amp;gt; chunk) throws Exception {
		for (CustomerDto customer: chunk) {
			log.info(&quot;Call Porcess in CustomItemWriter...&quot;);
			customService.processToOtherService(customer);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;전체코드&lt;/h2&gt;
&lt;pre id=&quot;code_1733818047122&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class MybatisItemWriterJobConfig {

	/**
	 * CHUNK 크기를 지정한다.
	 */
	public static final int CHUNK_SIZE = 100;
	public static final String ENCODING = &quot;UTF-8&quot;;
	public static final String MY_BATIS_ITEM_WRITER = &quot;MY_BATIS_ITEM_CUSTOM_WRITER&quot;;

	private final DataSource dataSource;
	private final SqlSessionFactory sqlSessionFactory;
	private final CustomItemWriter customItemWriter;

	@Bean
	public FlatFileItemReader&amp;lt;CustomerDto&amp;gt; flatFileItemReader9() {

		return new FlatFileItemReaderBuilder&amp;lt;CustomerDto&amp;gt;()
			.name(&quot;flatFileItemReader9&quot;)
			.resource(new ClassPathResource(&quot;./customer.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;,&quot;)
			.names(&quot;name&quot;, &quot;age&quot;, &quot;gender&quot;)
			.targetType(CustomerDto.class)
			.build();
	}

	@Bean
	public Step flatFileStep9(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init flatFileStep -----------------&quot;);

		return new StepBuilder(&quot;flatFileStep&quot;, jobRepository)
			.&amp;lt;CustomerDto, CustomerDto&amp;gt;chunk(CHUNK_SIZE, transactionManager)
			.reader(flatFileItemReader9())
			.writer(customItemWriter)
			.build();
	}

	@Bean
	public Job flatFileJob9(@Qualifier(&quot;flatFileStep9&quot;) Step flatFileStep, JobRepository jobRepository) {
		log.info(&quot;------------------ Init flatFileJob -----------------&quot;);
		return new JobBuilder(MY_BATIS_ITEM_WRITER, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(flatFileStep)
			.build();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/15</guid>
      <comments>https://j0free.tistory.com/15#entry15comment</comments>
      <pubDate>Tue, 10 Dec 2024 17:07:45 +0900</pubDate>
    </item>
    <item>
      <title>8. CompositeItemProcessor 으로 여러단계에 걸쳐 데이터 Transform하기</title>
      <link>https://j0free.tistory.com/14</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;CompositeItemProcessor는 Spring Batch에서 제공하는 클래스로, 여러 ItemProcessor를 하나로 결합하여 순차적으로 데이터를 처리할 수 있게 해줍니다. &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;CompositeItemProcessor를 사용하여 여러개의 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;ItemProcessor간의 체이닝이 가능합니다.&lt;/span&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;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;하나의 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;ItemProcessor에 여러 작업을 하도록 구현 할 수 있지만, &lt;span style=&quot;color: #555555; text-align: start;&quot;&gt;Processor 하나에 역할이 커지고 책임이 많아 지기 때문에 작은 사이즈의 &lt;span style=&quot;color: #555555; text-align: start;&quot;&gt;Processor로 구현 한 뒤에 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;CompositeItemProcessor로 체이닝을 통해 사용합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한, Processor를 재사용하여 다른 Job에서도 활용할 수 있습니다. &lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;다양한 ItemProcessor를 조합하여 원하는 처리 과정을 구현할 수 있다는 장점도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;LowerCaseItemProcessor 작성하기&lt;/h3&gt;
&lt;pre id=&quot;code_1733130023551&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.schooldevops.springbatch.batchsample.jobs.mybatis;

import com.schooldevops.springbatch.batchsample.jobs.models.Customer;
import org.springframework.batch.item.ItemProcessor;

/**
 * 이름, 성별을 소문자로 변경하는 ItemProcessor
 */
public class LowerCaseItemProcessor implements ItemProcessor&amp;lt;Customer, Customer&amp;gt; {
    @Override
    public Customer process(Customer item) throws Exception {
        item.setName(item.getName().toLowerCase());
        item.setGender(item.getGender().toLowerCase());
        return item;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;After20YearsItemProcessor 작성하기&lt;/h3&gt;
&lt;pre id=&quot;code_1733130052949&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.schooldevops.springbatch.batchsample.jobs.mybatis;

import com.schooldevops.springbatch.batchsample.jobs.models.Customer;
import org.springframework.batch.item.ItemProcessor;

/**
 * 나이에 20년을 더하는 ItemProcessor
 */
public class After20YearsItemProcessor implements ItemProcessor&amp;lt;Customer, Customer&amp;gt; {
    @Override
    public Customer process(Customer item) throws Exception {
        item.setAge(item.getAge() + 20);
        return item;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;CompositeItemProcess 구현하기&lt;/h3&gt;
&lt;pre id=&quot;code_1733130074190&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
    public CompositeItemProcessor&amp;lt;Customer, Customer&amp;gt; compositeItemProcessor() {
        return new CompositeItemProcessorBuilder&amp;lt;Customer, Customer&amp;gt;()
                .delegates(List.of(
                        new LowerCaseItemProcessor(),
                        new After20YearsItemProcessor()
                ))
                .build();
    }&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;CompositeItemProcessorBuilder를 이용하여 delegates를 통해서 ItemProcessor가 수행할 순서대로 배열을 만들어 전달했다.&lt;/li&gt;
&lt;li&gt;compositeItemProcessor를 processor에 넣는다면 원하는 작업을 수행하는 것을 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/14</guid>
      <comments>https://j0free.tistory.com/14#entry14comment</comments>
      <pubDate>Mon, 2 Dec 2024 18:03:34 +0900</pubDate>
    </item>
    <item>
      <title>7. MyBatisPagingItemReader로 DB내용을 읽고, MyBatisItemWriter로 DB에 쓰기</title>
      <link>https://j0free.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #212121; text-align: start;&quot;&gt;MyBatis는 자바 기반의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;SQL Mapper&lt;/b&gt;&lt;span style=&quot;color: #212121; text-align: start;&quot;&gt;로, 애플리케이션에서 데이터베이스와 상호작용하기 위한 도구입니다. 주로 SQL 문을 직접 작성하고 이를 자바 코드와 연결하는 데 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;section-0&quot; style=&quot;color: #212121; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. MyBatisItemReader&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #212121; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #212121; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;MyBatisPagingItemReader Spring Mybatis에서 제공하는 ItemReader 인터페이스를 구현하는 클래스이다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MyBatis의 Object Relation Mapper를 이용하며 다음과 같은 특징을 가지고 있다.&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;장점:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간편한 설정: MyBatis 쿼리 매퍼를 직접 활용하여 데이터를 읽을 수 있어 설정이 간편하다.&lt;/li&gt;
&lt;li&gt;쿼리 최적화: MyBatis의 다양한 기능을 활용하여 최적화된 쿼리를 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;동적 쿼리 지원: 런타임 시 조건에 따라 동적으로 쿼리를 생성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;단점:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MyBatis 의존성: MyBatis 라이브러리에 의존해야 한다.&lt;/li&gt;
&lt;li&gt;커스터마이징 복잡: Chunk-oriented Processing 방식과 비교했을 때 커스터마이징이 더 복잡할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. MyBatisPagingItemReader 작성하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731923181732&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	@Bean
	public MyBatisPagingItemReader&amp;lt;Customer&amp;gt; myBatisItemReader() throws Exception {

		return new MyBatisPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
			.sqlSessionFactory(sqlSessionFactory)
			.pageSize(CHUNK_SIZE)
			.queryId(&quot;com.schooldevops.springbatch.batchsample.jobs.selectCustomers&quot;)
			.build();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SqlSessionFactory: MyBatis 설정 정보 및 SQL 쿼리 매퍼 정보를 담고 있는 객체이다.&lt;/li&gt;
&lt;li&gt;QueryId: 데이터를 읽을 MyBatis 쿼리 ID이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 할 쿼리 xml의 mapper의 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PageSize: 페이징 쿼리를 위한 페이지 크기를 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. query 작성하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731924908546&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;

&amp;lt;mapper namespace=&quot;com.schooldevops.springbatch.batchsample.jobs&quot;&amp;gt;

    &amp;lt;resultMap id=&quot;customerResult&quot; type=&quot;com.schooldevops.springbatch.batchsample.config.Customer&quot;&amp;gt;
        &amp;lt;result property=&quot;id&quot; column=&quot;id&quot;/&amp;gt;
        &amp;lt;result property=&quot;name&quot; column=&quot;name&quot;/&amp;gt;
        &amp;lt;result property=&quot;age&quot; column=&quot;age&quot;/&amp;gt;
        &amp;lt;result property=&quot;gender&quot; column=&quot;gender&quot;/&amp;gt;
    &amp;lt;/resultMap&amp;gt;

    &amp;lt;select id=&quot;selectCustomers&quot; resultMap=&quot;customerResult&quot;&amp;gt;
        SELECT id, name, age, gender
        FROM customer2
                 LIMIT #{_skiprows}, #{_pagesize}
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;resultMap는 select 쿼리 실행 결과를&amp;nbsp; table column과 객체를 매핑한다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;_skiprows: 오프셋. 쿼리 별과에서 얼마나 스킵할지 지정. pageSize를 지정했다면 자동으로 계산&lt;/li&gt;
&lt;li&gt;&amp;nbsp;_pagesize: 한 번에 가져올 페이지 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 의존성 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731925127565&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.mybatis:mybatis:3.5.16'
implementation 'org.mybatis:mybatis-spring:3.0.3'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle에 mybatis 의존성을 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. mybatis-config.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731925196262&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE configuration PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;settings&amp;gt;
        &amp;lt;setting name=&quot;mapUnderscoreToCamelCase&quot; value=&quot;true&quot; /&amp;gt;
        &amp;lt;setting name=&quot;multipleResultSetsEnabled&quot; value=&quot;true&quot; /&amp;gt;
        &amp;lt;setting name=&quot;callSettersOnNulls&quot; value=&quot;true&quot;/&amp;gt;
    &amp;lt;/settings&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mybatis 설정 xml을 classpath에 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 설정은 아래 참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;MyBatis 공식 문서 - 매퍼 설정&quot; href=&quot;https://mybatis.org/mybatis-3/ko/configuration.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mybatis.org/mybatis-3/ko/configuration.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731925332759&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;mybatis &amp;ndash; 마이바티스 3 | 매퍼 설정&quot; data-og-description=&quot;매퍼 설정 마이바티스 XML 설정파일은 다양한 설정과 프로퍼티를 가진다. 문서의 구조는 다음과 같다.: configuration properties 이 설정은 외부에 옮길 수 있다. 자바 프로퍼티 파일 인스턴스에 설정할&quot; data-og-host=&quot;mybatis.org&quot; data-og-source-url=&quot;https://mybatis.org/mybatis-3/ko/configuration.html&quot; data-og-url=&quot;https://mybatis.org/mybatis-3/ko/configuration.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://mybatis.org/mybatis-3/ko/configuration.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mybatis.org/mybatis-3/ko/configuration.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;mybatis &amp;ndash; 마이바티스 3 | 매퍼 설정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;매퍼 설정 마이바티스 XML 설정파일은 다양한 설정과 프로퍼티를 가진다. 문서의 구조는 다음과 같다.: configuration properties 이 설정은 외부에 옮길 수 있다. 자바 프로퍼티 파일 인스턴스에 설정할&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mybatis.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6.&amp;nbsp;전체 코드 보기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731925364467&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class MyBatisReaderJobConfig {

	/**
	 * CHUNK 크기를 지정한다.
	 */
	public static final int CHUNK_SIZE = 2;
	public static final String ENCODING = &quot;UTF-8&quot;;
	public static final String MYBATIS_CHUNK_JOB = &quot;MYBATIS_CHUNK_JOB&quot;;

	private final DataSource dataSource;
	private final SqlSessionFactory sqlSessionFactory;

	@Bean
	public MyBatisPagingItemReader&amp;lt;Customer&amp;gt; myBatisItemReader() throws Exception {

		return new MyBatisPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
			.sqlSessionFactory(sqlSessionFactory)
			.pageSize(CHUNK_SIZE)
			.queryId(&quot;com.schooldevops.springbatch.batchsample.jobs.selectCustomers&quot;)
			.build();
	}


	@Bean
	public FlatFileItemWriter&amp;lt;Customer&amp;gt; customerCursorFlatFileItemWriter() {
		return new FlatFileItemWriterBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;customerCursorFlatFileItemWriter&quot;)
			.resource(new FileSystemResource(&quot;./customer_new_v4.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;\t&quot;)
			.names(&quot;Name&quot;, &quot;Age&quot;, &quot;Gender&quot;)
			.build();
	}


	@Bean
	public Step customerJdbcCursorStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) throws Exception {
		log.info(&quot;------------------ Init customerJdbcCursorStep -----------------&quot;);

		return new StepBuilder(&quot;customerJdbcCursorStep&quot;, jobRepository)
			.&amp;lt;Customer, Customer&amp;gt;chunk(CHUNK_SIZE, transactionManager)
			.reader(myBatisItemReader())
			.processor(new CustomerItemProcessor())
			.writer(customerCursorFlatFileItemWriter())
			.build();
	}

	@Bean
	public Job customerJdbcCursorPagingJob(@Qualifier(&quot;customerJdbcCursorStep&quot;) Step customerJdbcCursorStep, JobRepository jobRepository) {
		log.info(&quot;------------------ Init customerJdbcCursorPagingJob -----------------&quot;);
		return new JobBuilder(MYBATIS_CHUNK_JOB, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(customerJdbcCursorStep)
			.build();
	}
}&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;여기서는 MyBatis를 이용하여, 페이징을 이용한 청크단위 처리를 수행했다.&lt;/li&gt;
&lt;li&gt;Mybatis는 쿼리를 직접 작성하고, 쿼리 내용직접 최적화 할수 있는 장점이 있다.&lt;/li&gt;
&lt;li&gt;pageSize를 이용하면 자동으로 offset과 pagesize를 자동으로 계산하여 페이징 처리를 수행해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;7. MyBatisItemWriter&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731926946008&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   @Bean
    public MyBatisBatchItemWriter&amp;lt;Customer&amp;gt; mybatisItemWriter() {
        return new MyBatisBatchItemWriterBuilder&amp;lt;Customer&amp;gt;()
                .sqlSessionFactory(sqlSessionFactory)
                .statementId(&quot;com.schooldevops.springbatch.batchsample.jobs.insertCustomers&quot;)
                .build();
    }&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;MyBatisBatchItemWriter Spring Batch에서 제공하는 ItemWriter 인터페이스를 구현하는 클래스이다.&lt;/li&gt;
&lt;li&gt;데이터를 MyBatis를 통해 데이터베이스에 저장하는 데 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222;&quot; data-ke-size=&quot;size23&quot;&gt;구성 요소&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SqlSessionTemplate: MyBatis SqlSession 생성 및 관리를 위한 템플릿 객체이다.&lt;/li&gt;
&lt;li&gt;SqlSessionFactory: SqlSessionTemplate 생성을 위한 팩토리 객체이다.&lt;/li&gt;
&lt;li&gt;StatementId: 실행할 MyBatis SQL 맵퍼의 스테이tement ID이다.&lt;/li&gt;
&lt;li&gt;ItemToParameterConverter: 객체를 ParameterMap으로 변경할수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222;&quot; data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORM 연동: MyBatis를 통해 다양한 데이터베이스에 데이터를 저장할 수 있다.&lt;/li&gt;
&lt;li&gt;SQL 쿼리 분리: SQL 쿼리를 Java 코드로부터 분리하여 관리 및 유지 보수가 용이하다.&lt;/li&gt;
&lt;li&gt;유연성: 다양한 설정을 통해 원하는 방식으로 데이터를 저장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222;&quot; data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 복잡성: MyBatis 설정 및 SQL 맵퍼 작성이 복잡할 수 있다.&lt;/li&gt;
&lt;li&gt;데이터베이스 종속: 특정 데이터베이스에 종속적이다.&lt;/li&gt;
&lt;li&gt;오류 가능성: 설정 오류 시 데이터 손상 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731927016348&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   &amp;lt;insert id=&quot;insertCustomers&quot; parameterType=&quot;com.schooldevops.springbatch.batchsample.config.Customer&quot;&amp;gt;
        INSERT INTO customer2(name, age, gender) VALUES (#{name}, #{age}, #{gender});
    &amp;lt;/insert&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 추가 하기 위한 insert 쿼리를 작성한다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731926988599&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    public MyBatisBatchItemWriter&amp;lt;Customer&amp;gt; mybatisItemWriter() {
        return new MyBatisBatchItemWriterBuilder&amp;lt;Customer&amp;gt;()
                .sqlSessionFactory(sqlSessionFactory)
                .statementId(&quot;com.schooldevops.springbatch.batchsample.jobs.insertCustomers&quot;)
               .itemToParameterConverter(item -&amp;gt; {
                   Map&amp;lt;String, Object&amp;gt; parameter = new HashMap&amp;lt;&amp;gt;();
                   parameter.put(&quot;name&quot;, item.getName());
                   parameter.put(&quot;age&quot;, item.getAge());
                   parameter.put(&quot;gender&quot;, item.getGender());
                   return parameter;
               })
                .build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map을 이용하여 위와 같이 작성이 가능하다.&lt;/p&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/12</guid>
      <comments>https://j0free.tistory.com/12#entry12comment</comments>
      <pubDate>Mon, 18 Nov 2024 19:51:16 +0900</pubDate>
    </item>
    <item>
      <title>5. JdbcPagingItemReader로 DB내용을 읽고, JdbcBatchItemWriter로 DB에 쓰기</title>
      <link>https://j0free.tistory.com/11</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 csv 파일을 읽고 쓰는 것에 배웠습니다. 이번에는 DB에서 데이터를 읽고 CSV 파일로 쓰거나 CSV 파일을 읽고 DB에 저장하는 방법에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. JdbcPagingItemReader&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JdbcPagingItemReader는 Spring Batch에서 제공하는 ItemReader로, 데이터베이스로부터 데이터를 페이지 단위로 읽는다.&lt;/li&gt;
&lt;li&gt;대규모 데이터 처리 효율성: 메모리 사용량을 최소화하고 커밋 간격을 설정하여 대규모 데이터를 효율적으로 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;쿼리 최적화: SQL 쿼리를 직접 작성하여 최적화된 데이터 읽기가 가능하다.&lt;/li&gt;
&lt;li&gt;커서 제어: 데이터베이스 커서를 사용하여 데이터 순회를 제어할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 쿼리 Provider 생성하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730794484298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PagingQueryProvider queryProvider() throws Exception {
        SqlPagingQueryProviderFactoryBean queryProvider = new SqlPagingQueryProviderFactoryBean();
        queryProvider.setDataSource(dataSource);  // DB 에 맞는 PagingQueryProvider 를 선택하기 위함
        queryProvider.setSelectClause(&quot;id, name, age, gender&quot;);
        queryProvider.setFromClause(&quot;from customer&quot;);
        queryProvider.setWhereClause(&quot;where age &amp;gt;= :age&quot;);

        Map&amp;lt;String, Order&amp;gt; sortKeys = new HashMap&amp;lt;&amp;gt;(1);
        sortKeys.put(&quot;id&quot;, Order.DESCENDING);

        queryProvider.setSortKeys(sortKeys);

        return queryProvider.getObject();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SqlPagingQueryProviderFactoryBean: 쿼리 프로파이더 팩토리&lt;/li&gt;
&lt;li&gt;setDataSource: 데이터소스를 설정한다.&lt;/li&gt;
&lt;li&gt;setSelectClause: select에서 프로젝션할 필드 이름을 지정한다.&lt;/li&gt;
&lt;li&gt;setFromClause: 조회할 테이블&lt;/li&gt;
&lt;li&gt;setWhereClause: 조건절&lt;/li&gt;
&lt;li&gt;setSortKeys: 소트 키를 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. JdbcPagingItemReader 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730794600560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
 public JdbcPagingItemReader&amp;lt;Customer&amp;gt; jdbcPagingItemReader() throws Exception {
        Map&amp;lt;String, Object&amp;gt; parameterValue = new HashMap&amp;lt;&amp;gt;();
        parameterValue.put(&quot;age&quot;, 20);

        return new JdbcPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
                .name(&quot;jdbcPagingItemReader&quot;)
                .fetchSize(CHUNK_SIZE)
                .dataSource(dataSource)
                .rowMapper(new BeanPropertyRowMapper&amp;lt;&amp;gt;(Customer.class))
                .queryProvider(queryProvider())
                .parameterValues(parameterValue)
                .build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ queryProvider를 이용해서 만든 쿼리에 조건을 넣고 CHUNK_SIZE 만큼 읽는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 전체 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730795168021&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class JdbcPagingReaderJobConfig {

	public static final int CHUNK_SIZE = 2;
	public static final String ENCODING = &quot;UTF-8&quot;;
	public static final String JDBC_PAGING_CHUNK_JOB = &quot;JDBC_PAGING_CHUNK_JOB&quot;;

	private final DataSource dataSource;

	@Bean
	public JdbcPagingItemReader&amp;lt;Customer&amp;gt; jdbcPagingItemReader() throws Exception {
		Map&amp;lt;String, Object&amp;gt; parameterValue = new HashMap&amp;lt;&amp;gt;();
		parameterValue.put(&quot;age&quot;, 20);

		return new JdbcPagingItemReaderBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;jdbcPagingItemReader&quot;)
			.fetchSize(CHUNK_SIZE)
			.dataSource(dataSource)
			.rowMapper(new BeanPropertyRowMapper&amp;lt;&amp;gt;(Customer.class))
			.queryProvider(queryProvider())
			.parameterValues(parameterValue)
			.build();
	}

	@Bean
	public PagingQueryProvider queryProvider() throws Exception {
		SqlPagingQueryProviderFactoryBean queryProvider = new SqlPagingQueryProviderFactoryBean();
		queryProvider.setDataSource(dataSource);
		queryProvider.setSelectClause(&quot;id, name, age, gender&quot;);
		queryProvider.setFromClause(&quot;from customer2&quot;);
		queryProvider.setWhereClause(&quot;where age &amp;gt;= :age&quot;);

		Map&amp;lt;String, Order&amp;gt; sortKeys = new HashMap&amp;lt;&amp;gt;(1);
		sortKeys.put(&quot;id&quot;, Order.DESCENDING);

		queryProvider.setSortKeys(sortKeys);

		return queryProvider.getObject();
	}

	@Bean
	public FlatFileItemWriter&amp;lt;Customer&amp;gt; customerFlatFileItemWriter() {
		return new FlatFileItemWriterBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;customerFlatFileItemWriter&quot;)
			.resource(new FileSystemResource(&quot;./customer_new_v1.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;\t&quot;)
			.names(&quot;Name&quot;, &quot;Age&quot;, &quot;Gender&quot;)
			.build();
	}

	@Bean
	public Step customerJdbcPagingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) throws Exception {
		log.info(&quot;------------------ Init customerJdbcPagingStep -----------------&quot;);

		return new StepBuilder(&quot;customerJdbcPagingStep&quot;, jobRepository)
			.&amp;lt;Customer, Customer&amp;gt;chunk(CHUNK_SIZE, transactionManager)
			.reader(jdbcPagingItemReader())
			.writer(customerFlatFileItemWriter())
			.build();
	}

	@Bean
	public Job customerJdbcPagingJob(@Qualifier(&quot;customerJdbcPagingStep&quot;) Step customerJdbcPagingStep, JobRepository jobRepository) {
		log.info(&quot;------------------ Init customerJdbcPagingJob -----------------&quot;);
		return new JobBuilder(JDBC_PAGING_CHUNK_JOB, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(customerJdbcPagingStep)
			.build();
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ DB에서 데이터를 읽고 CSV 파일에 읽은 데이터를 쓰고 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. DB 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730795385590&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;clusterInstanceHostPattern=?&amp;amp;zeroDateTimeBehavior=CONVERT_TO_NULL&amp;amp;allowMultiQueries=true
    username: root
    password: root1234&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* DB에 데이터를 데이터를 넣어놔야 읽고 CSV 파일로 만들 수 있기 때문에, docker를 이용해서 테이블 및 데이터를 준비 해둔다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7.&amp;nbsp; JdbcBatchItemWriter&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JdbcBatchItemWriter Spring Batch에서 제공하는 ItemWriter 인터페이스를 구현하는 클래스이다.&lt;/li&gt;
&lt;li&gt;데이터를 JDBC를 통해 데이터베이스에 저장하는 데 사용된다.&lt;/li&gt;
&lt;li&gt;DataSource: 데이터베이스 연결 정보를 지정한다.&lt;/li&gt;
&lt;li&gt;SqlStatementCreator: INSERT 쿼리를 생성하는 역할을 한다.&lt;/li&gt;
&lt;li&gt;PreparedStatementSetter: INSERT 쿼리의 파라미터 값을 설정하는 역할을 한다.&lt;/li&gt;
&lt;li&gt;ItemSqlParameterSourceProvider: Item 객체를 기반으로 PreparedStatementSetter에 전달할 파라미터 값을 생성하는 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 연동: JDBC를 통해 다양한 데이터베이스에 데이터를 저장할 수 있다.&lt;/li&gt;
&lt;li&gt;성능: 대량의 데이터를 빠르게 저장할 수 있다.&lt;/li&gt;
&lt;li&gt;유연성: 다양한 설정을 통해 원하는 방식으로 데이터를 저장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 복잡성: JDBC 설정 및 쿼리 작성이 복잡할 수 있다.&lt;/li&gt;
&lt;li&gt;데이터베이스 종속: 특정 데이터베이스에 종속적이다.&lt;/li&gt;
&lt;li&gt;오류 가능성: 설정 오류 시 데이터 손상 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8 . 테이블 생성하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730795786610&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table testdb.customer2
(
    id     int auto_increment primary key,
    name   varchar(100) null,
    age    int          null,
    gender varchar(10)  null
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;9. SqlPatameterSourceProvider&amp;nbsp;작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730795870440&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomerItemSqlParameterSourceProvider implements ItemSqlParameterSourceProvider&amp;lt;Customer&amp;gt; {
    @Override
    public SqlParameterSource createSqlParameterSource(Customer item) {
        return new BeanPropertySqlParameterSource(item);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;10. 전체코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730795900939&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class JdbcBatchItemJobConfig {

	/**
	 * CHUNK 크기를 지정한다.
	 */
	public static final int CHUNK_SIZE = 100;
	public static final String ENCODING = &quot;UTF-8&quot;;
	public static final String JDBC_BATCH_WRITER_CHUNK_JOB = &quot;JDBC_BATCH_WRITER_CHUNK_JOB&quot;;

	private final DataSource dataSource;

	@Bean
	public FlatFileItemReader&amp;lt;Customer&amp;gt; jdbcFlatFileItemReader() {

		return new FlatFileItemReaderBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;FlatFileItemReader&quot;)
			.resource(new ClassPathResource(&quot;./customer.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;,&quot;)
			.names(&quot;name&quot;, &quot;age&quot;, &quot;gender&quot;)
			.targetType(Customer.class)
			.build();
	}

	@Bean
	public JdbcBatchItemWriter&amp;lt;Customer&amp;gt; jdbcFlatFileItemWriter() {

		return new JdbcBatchItemWriterBuilder&amp;lt;Customer&amp;gt;()
			.dataSource(dataSource)
			.sql(&quot;INSERT INTO customer2 (name, age, gender) VALUES (:name, :age, :gender)&quot;)
			.itemSqlParameterSourceProvider(new CustomerItemSqlParameterSourceProvider())
			.build();
	}


	@Bean
	public Step jdbcFlatFileStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init flatFileStep -----------------&quot;);

		return new StepBuilder(&quot;jdbcFlatFileStep&quot;, jobRepository)
			.&amp;lt;Customer, Customer&amp;gt;chunk(CHUNK_SIZE, transactionManager)
			.reader(jdbcFlatFileItemReader())
			.writer(jdbcFlatFileItemWriter())
			.build();
	}

	@Bean
	public Job jdbcBatchWriterChunkJob(@Qualifier(&quot;jdbcFlatFileStep&quot;) Step flatFileStep, JobRepository jobRepository) {
		log.info(&quot;------------------ Init flatFileJob -----------------&quot;);
		return new JobBuilder(JDBC_BATCH_WRITER_CHUNK_JOB, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(flatFileStep)
			.build();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* CSV 파일을 읽고 객체와 매핑 후 DB에 데이터를 저장한다.&lt;/p&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/11</guid>
      <comments>https://j0free.tistory.com/11#entry11comment</comments>
      <pubDate>Tue, 5 Nov 2024 17:38:58 +0900</pubDate>
    </item>
    <item>
      <title>4. FlatFileItemReader로 단순 파일 읽고, FlatFileItemWriter로 파일에 쓰기</title>
      <link>https://j0free.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;CSV 파일을 읽고 내용을 집계하여 CSV 파일로 쓰도록 &lt;b&gt;FlatFileItemReader&lt;/b&gt;와 &lt;b&gt;FlatFileItemWriter&lt;/b&gt;에 대해 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.FlatFileItemReader&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;FlatFileItemReader는 Spring Batch에서 제공하는 기본적인 ItemReader로, 텍스트 파일로부터 데이터를 읽습니다.&lt;/li&gt;
&lt;li&gt;고정 길이, 구분자 기반, 멀티라인 등 다양한 형식의 텍스트 파일을 지원하며, 다음과 같은 장점을 가집니다.&lt;/li&gt;
&lt;li&gt;간단하고 효율적인 구현: 설정 및 사용이 간편하며, 대규모 데이터 처리에도 효율적입니다.&lt;/li&gt;
&lt;li&gt;다양한 텍스트 파일 형식 지원: 고정 길이, 구분자 기반, 멀티라인 등 다양한 형식의 텍스트 파일을 읽을 수 있습니다.&lt;/li&gt;
&lt;li&gt;확장 가능성: 토크나이저, 필터 등을 통해 기능을 확장할 수 있습니다.&lt;/li&gt;
&lt;li&gt;사용처: 고정 길이, 구분자 기반, 멀티라인 등 다양한 형식의 텍스트 파일 데이터 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FlatFileItemReader 장점&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FlatFileItemReader 단점&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;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.FlatFileItemWriter&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;FlatFileItemWriter는 Spring Batch에서 제공하는 ItemWriter 인터페이스를 구현하는 클래스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;데이터를 텍스트 파일로 출력하는 데 사용&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;FlatFileItemWriter&lt;/b&gt; 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간편성: 텍스트 파일로 데이터를 출력하는 간편한 방법을 제공한다.&lt;/li&gt;
&lt;li&gt;유연성: 다양한 설정을 통해 원하는 형식으로 출력 파일을 만들 수 있다.&lt;/li&gt;
&lt;li&gt;성능: 대량의 데이터를 빠르게 출력할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;FlatFileItemWriter&lt;/b&gt;&lt;span&gt; 단&lt;/span&gt;점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. CSV로 부터 정보를 읽기 위한 객체 정의&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730707491706&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Setter
@Getter
public class Customer {

	private String name;
	private int age;
	private String gender;

	public Customer() {
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ Customer 객체에 Setter와 기본 생성자가 필요한 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 객체 생성 및 필드 값 할당을 위한 기본 생성자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FlatFileItemReader&lt;/span&gt;는 리플렉션(reflection)을 사용하여 각 레코드를 읽고 매핑할 객체의 인스턴스를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;이때, &lt;b&gt;기본 생성자&lt;/b&gt;가 없으면 객체를 생성할 수 없으므로 &lt;span&gt;FlatFileItemReader&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;b&gt;2. CSV 데이터 값을 객체 필드에 할당하기 위한 Setter 메서드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FlatFileItemReader&lt;/span&gt;는 각 CSV 데이터 필드를 매핑할 객체의 필드에 할당하기 위해 &lt;b&gt;Setter 메서드&lt;/b&gt;를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;예를 들어, CSV 파일의 &lt;span&gt;name&lt;/span&gt;, &lt;span&gt;age&lt;/span&gt;, &lt;span&gt;gender&lt;/span&gt; 데이터를 &lt;span&gt;Customer&lt;/span&gt; 객체에 할당하려면 &lt;span&gt;Customer&lt;/span&gt; 클래스에 &lt;span&gt;setName()&lt;/span&gt;, &lt;span&gt;setAge()&lt;/span&gt;, &lt;span&gt;setGender()&lt;/span&gt; 메서드가 필요합니다. 이 방법으로 &lt;span&gt;FlatFileItemReader&lt;/span&gt;는 필드 값을 각 필드에 쉽게 할당할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;만약 &lt;span&gt;setter&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;b&gt;요약&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;기본 생성자&lt;/b&gt;는 객체 인스턴스를 만들기 위해 필수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;Setter 메서드&lt;/b&gt;는 리플렉션을 통해 CSV의 각 필드 값을 객체의 필드에 할당하기 위해 필요합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4&lt;/b&gt;.&lt;b&gt;FlatFileItemReader 작성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1730707693838&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public FlatFileItemReader&amp;lt;Customer&amp;gt; flatFileItemReader() {

		return new FlatFileItemReaderBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;FlatFileItemReader&quot;)
			.resource(new ClassPathResource(&quot;./customer.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;,&quot;)
			.names(&quot;name&quot;, &quot;age&quot;, &quot;gender&quot;)
			.targetType(Customer.class)
			.build();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;name : &lt;b&gt;FlatFileItemReader&amp;nbsp;&lt;/b&gt;빈의 이름을 지정한다.&lt;/li&gt;
&lt;li&gt;resource : 읽어들일 파일의 위치를 지정한다.&lt;/li&gt;
&lt;li&gt;encoding : 파일의 인코딩을 지정한다.&lt;/li&gt;
&lt;li&gt;delimited().delimiter() : 읽어들일 정보간 구분자를 지정한다.&lt;/li&gt;
&lt;li&gt;names : 구분자로 구분된 데이터의 이름을 지정한다.&lt;/li&gt;
&lt;li&gt;targerType : 매핑할 객체 타입을 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. FlatFileItemWriter&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730707918784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public FlatFileItemWriter&amp;lt;Customer&amp;gt; flatFileItemWriter() {

		return new FlatFileItemWriterBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;flatFileItemWriter&quot;)
			.resource(new FileSystemResource(&quot;./customer_new.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;\t&quot;)
			.names(&quot;Name&quot;, &quot;Age&quot;, &quot;Gender&quot;)
			.append(false)
			.lineAggregator(new CustomerLineAggregator())
			.headerCallback(new CustomerHeader())
			.footerCallback(new CustomerFooter(aggregateInfos))
			.build();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;name :&lt;span&gt; &lt;b&gt;FlatFileItemWriter&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;빈의 이름을 지정한다.&lt;/li&gt;
&lt;li&gt;resource : 읽어들일 파일의 위치를 지정한다.&lt;/li&gt;
&lt;li&gt;encoding : 파일의 인코딩을 지정한다.&lt;/li&gt;
&lt;li&gt;delimited().delimiter() : 읽어들일 정보간 구분자를 지정한다.&lt;/li&gt;
&lt;li&gt;names : 구분자로 구분된 데이터의 이름을 지정한다.&lt;/li&gt;
&lt;li&gt;append : 기존 파일에 추가 할지 true or false로 지정한다.&lt;/li&gt;
&lt;li&gt;lineAggergator : 라인 구분자를 지정한다.&lt;/li&gt;
&lt;li&gt;headerCallback : 출력 파일의 헤더를 지정할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;footerCallback : &lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;출력 파일의 푸터를 지정할 수 있도록 한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;targerType : 매핑할 객체 타입을 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. CustomerLineAggregator 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730708583545&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomerLineAggregator implements LineAggregator&amp;lt;Customer&amp;gt; {
	@Override
	public String aggregate(Customer item) {
		return item.getName() + &quot;,&quot; + item.getAge();
	}
}&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;span style=&quot;color: #222222; text-align: left;&quot;&gt;FlatFile에 저장할 아이템들을 스트링으로 변환하는 방법을 지정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. CustomerHeader 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730708968889&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomerHeader implements FlatFileHeaderCallback {
	@Override
	public void writeHeader(Writer writer) throws IOException {
		writer.write(&quot;ID,AGE&quot;);
	}
}&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;FlatFileHeaderCallback은 writeHeader를 구현하고, 출력될 파일의 헤더를 달아주는 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. CustomerFooter 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730709058178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class CustomerFooter implements FlatFileFooterCallback {
	ConcurrentHashMap&amp;lt;String, Integer&amp;gt; aggregateCustomers;

	public CustomerFooter(ConcurrentHashMap&amp;lt;String, Integer&amp;gt; aggregateCustomers) {
		this.aggregateCustomers = aggregateCustomers;
	}

	@Override
	public void writeFooter(Writer writer) throws IOException {
		writer.write(&quot;총 고객 수: &quot; + aggregateCustomers.get(&quot;TOTAL_CUSTOMERS&quot;));
		writer.write(System.lineSeparator());
		writer.write(&quot;총 나이: &quot; + aggregateCustomers.get(&quot;TOTAL_AGES&quot;));
	}
}&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;FlatFileFooterCallback 은 푸터를 작성할때 사용한다.&lt;/li&gt;
&lt;li&gt;결과를 집계하여 총 고객수와 총 나이를 출력하고 있다.&lt;/li&gt;
&lt;li&gt;이때 전달된 HashMap을 전달하여, 결과를 출력하고 있음을 확인하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;9.&amp;nbsp;AggregateCustomerProcessor 작성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;csv로 부터 읽은 데이터를 객체에 매핑한 결과를 writer를 쓰기 전에 별도의 처리를 하는 클래스이다.&lt;/p&gt;
&lt;pre id=&quot;code_1730709182464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class AggregateCustomerProcessor implements ItemProcessor&amp;lt;Customer, Customer&amp;gt; {

	ConcurrentHashMap&amp;lt;String, Integer&amp;gt; aggregateCustomers;

	public AggregateCustomerProcessor(ConcurrentHashMap&amp;lt;String, Integer&amp;gt; aggregateCustomers) {
		this.aggregateCustomers = aggregateCustomers;
	}

	@Override
	public Customer process(Customer item) throws Exception {
		aggregateCustomers.putIfAbsent(&quot;TOTAL_CUSTOMERS&quot;, 0);
		aggregateCustomers.putIfAbsent(&quot;TOTAL_AGES&quot;, 0);

		aggregateCustomers.put(&quot;TOTAL_CUSTOMERS&quot;, aggregateCustomers.get(&quot;TOTAL_CUSTOMERS&quot;) + 1);
		aggregateCustomers.put(&quot;TOTAL_AGES&quot;, aggregateCustomers.get(&quot;TOTAL_AGES&quot;) + item.getAge());
		return item;
	}
}&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;span style=&quot;color: #222222; text-align: left;&quot;&gt;ItemProcessor은 process 메소드를 구현하였으며, 각 아이템을 하나씩 읽고 아이템의 내용을 집계&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체소스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730709222221&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
public class FlatFileItemJobConfig {

	/**
	 * CHUNK 크기를 지정한다.
	 */
	public static final int CHUNK_SIZE = 100;
	public static final String ENCODING = &quot;UTF-8&quot;;
	public static final String FLAT_FILE_WRITER_CHUNK_JOB = &quot;FLAT_FILE_WRITER_CHUNK_JOB&quot;;

	private ConcurrentHashMap&amp;lt;String, Integer&amp;gt; aggregateInfos = new ConcurrentHashMap&amp;lt;&amp;gt;();

	private final ItemProcessor&amp;lt;Customer, Customer&amp;gt; itemProcessor = new AggregateCustomerProcessor(aggregateInfos);

	@Bean
	public FlatFileItemReader&amp;lt;Customer&amp;gt; flatFileItemReader() {

		return new FlatFileItemReaderBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;FlatFileItemReader&quot;)
			.resource(new ClassPathResource(&quot;./customer.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;,&quot;)
			.names(&quot;name&quot;, &quot;age&quot;, &quot;gender&quot;)
			.targetType(Customer.class)
			.build();
	}

	@Bean
	public FlatFileItemWriter&amp;lt;Customer&amp;gt; flatFileItemWriter() {

		return new FlatFileItemWriterBuilder&amp;lt;Customer&amp;gt;()
			.name(&quot;flatFileItemWriter&quot;)
			.resource(new FileSystemResource(&quot;./customer_new.csv&quot;))
			.encoding(ENCODING)
			.delimited().delimiter(&quot;\t&quot;)
			.names(&quot;Name&quot;, &quot;Age&quot;, &quot;Gender&quot;)
			.append(false)
			.lineAggregator(new CustomerLineAggregator())
			.headerCallback(new CustomerHeader())
			.footerCallback(new CustomerFooter(aggregateInfos))
			.build();
	}


	@Bean
	public Step flatFileStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init flatFileStep -----------------&quot;);

		return new StepBuilder(&quot;flatFileStep&quot;, jobRepository)
			.&amp;lt;Customer, Customer&amp;gt;chunk(CHUNK_SIZE, transactionManager)
			.reader(flatFileItemReader())
			.processor(itemProcessor)
			.writer(flatFileItemWriter())
			.build();
	}

	@Bean
	public Job flatFileJob(@Qualifier(&quot;flatFileStep&quot;) Step flatFileStep, JobRepository jobRepository) {
		log.info(&quot;------------------ Init flatFileJob -----------------&quot;);

		return new JobBuilder(FLAT_FILE_WRITER_CHUNK_JOB, jobRepository)
			.incrementer(new RunIdIncrementer())
			.start(flatFileStep)
			.build();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 이전 3장에서 작성 한 batch가 있다면, step 및 job 구성 시에 @Qulifier를 이용하여 각 Job이 필요한 Bean을 넣도록 수정하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch는 초기 실행 시 Job, Step 인터페이스를 구현한 모든 Bean을 등록하려고 하기 때문에 이름 지정을 통해 필요한&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean을 주입하는 것이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ &lt;b&gt;Caused by: java.lang.IllegalArgumentException: Job name must be specified in case of multiple jobs 예외 발생&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 Spring Boot 3 버전에서부터 Spring Batch 사용 시 업데이트 된 내용 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 스프링 부트가 구동될 때 모든 Job 빈들을 읽어 실행하는 구조였다면, 새로운 버전에서부터는 복수 개의 Job이 컨텍스트 내에 정의되어 있다면 부트 구동 시점에 가동시킬 Job을 프로프티에 명시해야한다. (하나만 등록 가능)&lt;/p&gt;
&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;/span&gt;컨텍스트&lt;span&gt; &lt;/span&gt;내에&lt;span&gt; &lt;/span&gt;하나의&lt;span&gt; Job&lt;/span&gt;만&lt;span&gt; &lt;/span&gt;존재한다면&lt;span&gt;, &lt;/span&gt;해당&lt;span&gt; Job&lt;/span&gt;은&lt;span&gt; &lt;/span&gt;별도의&lt;span&gt; &lt;/span&gt;명시&lt;span&gt; &lt;/span&gt;없이&lt;span&gt; &lt;/span&gt;부트&lt;span&gt; &lt;/span&gt;구동&lt;span&gt; &lt;/span&gt;시점에&lt;span&gt; &lt;/span&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;b&gt;&lt;span&gt;해결방법&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730710104891&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml

spring:
  batch:
    job:
      name: myJob&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 구동 시 실행 할 Job을 명시해준다.&lt;/p&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/10</guid>
      <comments>https://j0free.tistory.com/10#entry10comment</comments>
      <pubDate>Mon, 4 Nov 2024 17:49:11 +0900</pubDate>
    </item>
    <item>
      <title>3.SpringBatch ChunkModel과 TaskletModel</title>
      <link>https://j0free.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBatch에서 데이터 처리 단위인 ChuckModel과 TaskletModel의 차이점에 대해 알아보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Chunk Model&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;Chunk Model은 처리할 데이터를 일정단위(청크) 로 처리하는 방식이다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #222222; text-align: start;&quot;&gt;ChunkOrientedTaskLet은 Spring Batch에서 제공하는 TaskLet 인터페이스의 구현체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;Chunk size 만큼 &lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;ItemReader, ItemProcessor, ItemWrite 처리하며, &lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;commit-interval이라는 설정값을 이용하여 조정이 가능&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #222222; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; letter-spacing: 0px;&quot;&gt;Chunk는 최대 Size만큼의 크기를 가진다. 예를 들어 20개의 소스를 처리해야하고, Chunk Size가 2인 경우 Chunk는 2개씩 처리되기 때문에 총 10번의 반복이 필요하다. 즉 10번의 트랜잭션을 탄다는 소리로 이해를 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #222222; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; letter-spacing: 0px;&quot;&gt;몇번의 트랙잭션을 수행 할 것인지도 Chunk Size를 통해 조정이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Chunk 단위의 트랜잭션 처리&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;각 Chunk가 읽기(Read), 처리(Process), 쓰기(Write) 작업을 거치고 성공적으로 끝나면, 그 Chunk는 트랜잭션이 커밋한다.&lt;/li&gt;
&lt;li&gt;예를 들어, 첫 번째 Chunk가 성공하면 트랜잭션이 커밋되고, 두 번째 Chunk가 실행된다.&lt;/li&gt;
&lt;li&gt;만약 두 번째 Chunk가 처리되는 도중에 예외(Exception)가 발생하면, 그 Chunk만 롤백한다.&lt;/li&gt;
&lt;li&gt;하지만 첫 번째 Chunk는 이미 성공적으로 커밋되었으므로 롤백되지 않습니다. 이는 각 Chunk가 별도의 트랜잭션으로 처리한다.&lt;/li&gt;
&lt;li&gt;Spring Batch는 데이터 처리의 안정성을 높이기 위해 Chunk 단위로 트랜잭션을 분리합니다. 이렇게 하면 대용량 데이터를 처리할 때, 부분적으로 성공한 Chunk는 다시 처리하지 않아도 되므로 성능을 향상시킬 수 있다.&lt;/li&gt;
&lt;li&gt;따라서, 이전 Chunk가 성공적으로 커밋되었고, 다음 Chunk에서 실패가 발생해도 이전 Chunk는 롤백되지 않는다. 이는 Chunk 단위로 트랜잭션을 관리하는 Spring Batch의 동작 방식 때문이다.&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;806&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MNuOM/btsKc26Qty0/n6WIdEh9QBZGTuNTbDlW81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MNuOM/btsKc26Qty0/n6WIdEh9QBZGTuNTbDlW81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MNuOM/btsKc26Qty0/n6WIdEh9QBZGTuNTbDlW81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMNuOM%2FbtsKc26Qty0%2Fn6WIdEh9QBZGTuNTbDlW81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;806&quot; height=&quot;407&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;407&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;ChunkOrientedTasklet 은 ItemReader, ItemProcessor, ItemWriter 구현체를 각각 호출&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #222222; text-align: left;&quot;&gt;Chunk 단위&amp;nbsp; 만큼 ItemReader, ItemProcessor, ItemWriter 구현체를 반복 실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. ItemReader&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;스프링배치에서는 이미 구현된 다양한 ItemReader구현체를 제공&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #222222; text-align: left;&quot;&gt;Chunk 단위&amp;nbsp; 만큼 데이터를 읽고 다음 &lt;/span&gt;ItemProcessor와 ItemWriter에게 전달한다.&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; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FlatFileItemReader:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플랫파일 (즉 구조화 되지 않은파일을 플랫파일이라고한다.)을 읽어 들인다.&lt;/li&gt;
&lt;li&gt;대표적인 것이 CSV파일 등이 있다.&lt;/li&gt;
&lt;li&gt;읽어들인 데이터를 객체로 매핑하기 위해서 delimeter를 기준으로 매핑 룰을 이용하여 객체로 매핑한다.&lt;/li&gt;
&lt;li&gt;혹은 입력에 대해서 Resource object를 이용하여 커스텀하게 매핑할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;StaxEventItemReader:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XML파일을 읽어 들인다.&lt;/li&gt;
&lt;li&gt;이름이 함축하듯이 XML파일을 StAX기반으로 읽어 들인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JdbcPagingItemReader / JdbcCursorItemReader:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC를 사용하여 SQL을 실행하고 데이터베이스의 레코드를 읽는다.&lt;/li&gt;
&lt;li&gt;데이터베이스에서 많은 양의 데이터를 처리해야 하는 경우에는 메모리에 있는 모든 레코드를 읽는 것을 피하고, 한 번의 처리에 필요한 데이터만 읽고 폐기하는 것이 필요하다.&lt;/li&gt;
&lt;li&gt;JdbcPagingItemReader는 JdbcTemplate을 이용하여 각 페이지에 대한 SELECT SQL을 나누어 처리하는 방식으로 구현된다.&lt;/li&gt;
&lt;li&gt;반면 JdbcCursorItemReader는 JDBC 커서를 이용하여 하나의 SELECT SQL을 발행하여 구현된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MyBatisCursorItemReader / MyBatisPagingItemReader:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MyBatis를 사용하여 데이터베이스의 레코드를 읽는다.&lt;/li&gt;
&lt;li&gt;MyBatis가 제공하는 Spring 조정 라이브러리는 MyBatis-Spring에서 제공된다.&lt;/li&gt;
&lt;li&gt;Paging과 Cursor의 차이점은 MyBatis를 구현방법이 다를뿐이지 JdbcXXXItemReader과 동일하다&lt;/li&gt;
&lt;li&gt;또한 ItemReaderJPA구현이나 Hibernate와 연동하여 데이터베이스의 레코드를 읽어오는 JpaPagingItemReader, HibernatePagingItemReader, HibernateCursor를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JmsItemReader / AmqpItemReader:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 JMS혹은 AMQP에서 읽어들인다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. ItemProcessor&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;ItemProcessor는 &lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;ItemReader를 통해 읽어온 데이터를 처리 및 가공하는 역할을 한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PassThroughItemProcessor:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&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;ValidatingItemProcessor:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력된 데이터를 체크한다.&lt;/li&gt;
&lt;li&gt;입력 확인 규칙을 구현하려면 Spring Batch 전용 org.springframework.batch.item.validator.Validator를 구현해야한다.&lt;/li&gt;
&lt;li&gt;그러나 일반적인 org.springframework.validation.Validator 의 어댑터인 SpringValidator와 org.springframework.validation의 규칙을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CompositeItemProcessor:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 입력 데이터에 대해 여러 ItemProcessor를 순차적으로 실행한다.&lt;/li&gt;
&lt;li&gt;ValidatingItemProcessor를 사용하여 입력 확인을 수행한 후 비즈니스 로직을 실행하려는 경우 활성화 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #222222;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. ItemWriter&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222;&quot; data-ke-size=&quot;size16&quot;&gt;ItemWriter는 &lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;ItemProcessor가 처리 한 데이터를 파일 및 DB에 저장하는 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FlatFileItemWriter:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리된 Java객체를 CSV 파일과 같은 플랫 파일로 작성한다.&lt;/li&gt;
&lt;li&gt;파일 라인에 대한 매핑 규칙은 구분 기호 및 개체에서 사용자 정의로 사용할수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;StaxEventItemWriter:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XML파일로 자바 객체를 쓰기할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JdbcBatchItemWriter:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC를 사용하여 SQL을 수행하고 자바 객체를 데이터베이스에 쓰기한다.&lt;/li&gt;
&lt;li&gt;내부적으로 JdbcTemplate를 사용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MyBatisBatchItemWriter:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mybatis를 사용하여 자바 객체를 데이터베이스로 쓰기한다.&lt;/li&gt;
&lt;li&gt;MyBatis-Spring 는 MyBatis에 의해서 제공되는 라이브러리를 이용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JmsItemWriter / AmqpItemWriter:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JMS혹은 AMQP로 자바 객체의 메시지를 전송한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #222222;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. Tasklet Model&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #222222; text-align: left;&quot;&gt;Chunk 단위로 데이터를 나눠서 처리하는 것이 아닌 한번에 하나의 레코드만 읽어서 처리 할 경우 &lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;Tasklet Model이 적합하다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #222222; text-align: left;&quot;&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;앞선, 2장에서 &lt;/span&gt;&lt;/span&gt;GreetingTask를 구현 한 것과 같이 사용자가 직접 인터페이스 구현을 해야한다.&lt;/p&gt;
&lt;p style=&quot;color: #222222;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SystemCommandTasklet:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 명령어를 비동기적으로 실행하는 Tasklet이다.&lt;/li&gt;
&lt;li&gt;명령 속성에 수행해야할 명령어를 지정하여 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;시스템 명령은 호출하는 스레드와 다른 스레드에 의해 실행되므로 프로세스 도중 타임아웃을 설정하고, 시스템 명령의 실행 스레드를 취소할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MethodInvokingTaskletAdapter:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POJO클래스의 특정 메소드를 실행하기 위한 태스클릿이다.&lt;/li&gt;
&lt;li&gt;targetObject 속성에 대상 클래스의 빈을 지정하고, targetMethod속성에 실행할 메소드 이름을 지정한다.&lt;/li&gt;
&lt;li&gt;POJO 클래스는 일괄 처리 종료 상태를 메소드의 반환 값으로 반환이 가능하지만, 이경우 사실은 ExitStatus를 반환값으로 설정해야한다.&lt;/li&gt;
&lt;li&gt;다른 타입의 값이 반환될 경우 반환값과 상관없이 &quot;정상 종료(ExitStatus:COMPLETED)&quot; 상태로 간주된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/9</guid>
      <comments>https://j0free.tistory.com/9#entry9comment</comments>
      <pubDate>Mon, 21 Oct 2024 01:36:40 +0900</pubDate>
    </item>
    <item>
      <title>2.SpringBatch 코드 설명 및 아키텍처 알아보기</title>
      <link>https://j0free.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://j0free.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;앞선 과정&lt;/a&gt;에서 SpringBatch 프로젝트를 구성 및 실행을 해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Job, step에 대해 알아보고 SpringBatch 아키텍처에 대해 학습해보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1.Spring Batch 아키텍처&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2KSrD/btsKd0mNLqf/KTmGqVQWe1FM7dHTHKdWT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2KSrD/btsKd0mNLqf/KTmGqVQWe1FM7dHTHKdWT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2KSrD/btsKd0mNLqf/KTmGqVQWe1FM7dHTHKdWT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2KSrD%2FbtsKd0mNLqf%2FKTmGqVQWe1FM7dHTHKdWT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;364&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Job&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Batch에서 일괄 적용을 위한 일련의 프로세스를 요약하는&lt;b&gt; 단일 실행 단위&lt;/b&gt;가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;Step&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Job을 구성하는 처리단위&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;u&gt;하나의 Job에는 여러 Step이 들어갈 수 있다. 1: N 구조로 되어있다.&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;하나의 Job에 여러 Step을 재사용, 병렬화, 조건분기 등을 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;Step은 tasklet 모델 / chunk 모델의 구현체가 탑재되어 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobLauncher&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Job을 수행하기 위한 인터페이스&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;JobLauncher는 사용자에 의해서 직접 수행된다.&lt;/li&gt;
&lt;li&gt;자바 커맨드를 통해서 CommandLineJobRunner 를 실행하여 단순하게 배치 프로세스가 수행될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ItemReader&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;청크단위 모델에서 사용하며, &lt;b&gt;소스 데이터를 읽어 들이는 역할&lt;/b&gt;을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ItemProcessor&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;읽어들인 청크 데이터를 처리&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;데이터 변환을 수행하거나, 데이터를 정제하는 등의 역할을 담당한다.&lt;/li&gt;
&lt;li&gt;옵션으로 필요없다면 사용하지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ItemWriter&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;청크 데이터를 읽어들였거나, 처리된 데이터를 실제 쓰기작업&lt;/b&gt;을 담당한다.&lt;/li&gt;
&lt;li&gt;데이터베이스로 저장하거나, 수정하는 역할을 할 수 있고, 파일로 처리결과를 출력할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tasklet&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단순하고 유연하게 배치 처리&lt;/b&gt;를 수행하는 태스크를 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobRepository&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job과 Step의 상태를 관리하는 시스템이다.&lt;/li&gt;
&lt;li&gt;스프링배치에서 사용하는 테이블 스키마를 기반으로 &lt;b&gt;상태정보를 저장하고 관리&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 스프링배치 흐름&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞세 살펴봤던 아키텍처에서 배치의 흐름이 어떻게 흘러가는지 알아보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;901&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brVh0t/btsKdfkG2eK/sb2GStk92c0rFULovkgOFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brVh0t/btsKdfkG2eK/sb2GStk92c0rFULovkgOFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brVh0t/btsKdfkG2eK/sb2GStk92c0rFULovkgOFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrVh0t%2FbtsKdfkG2eK%2Fsb2GStk92c0rFULovkgOFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;901&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;901&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;처리흐름 관점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobScheduler 가 배치를 트리거링 하면&amp;nbsp; -&amp;gt; JobLauncher 를 실행&lt;/li&gt;
&lt;li&gt;JobLauncher 는 Job을 실행 -&amp;gt; JobExecution 을 수행하고, Execution Context 정보를 이용한다.&lt;/li&gt;
&lt;li&gt;Job은 -&amp;gt;Step을 실행한다. 이때 StepExecution을 수행하고, Execution Context 정보가 전달되어 수행된다.&lt;/li&gt;
&lt;li&gt;Step은 Tasklet과 Chunk모델을 가지고 있으며 위 그림에서는 Chunk 모델로 수행되게 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk 모델은 ItemReader를 통해서 소스 데이터를 읽어 들인다.&lt;/li&gt;
&lt;li&gt;ItemProcessor를 통해서 읽어들인 청크단위 데이터를 처리한다. 처리는 데이터를 변환하거나 가공하는 역할을 하게 된다.&lt;/li&gt;
&lt;li&gt;ItemWriter는 처리된 청크 데이터를 쓰기작업한다. 다양한 Writer를 통해 데이터베이스에 저장하거나, 파일로 쓰는 역할을 하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Batch에서 Chunk란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Chunk&lt;/b&gt;는 Spring Batch에서 대량 데이터를 처리할 때 효율적인 방법 중 하나로, 데이터를 &lt;b&gt;읽기(Read)&lt;/b&gt;, &lt;b&gt;처리(Process)&lt;/b&gt;, **쓰기(Write)**의 단위로 나누어 실행하는 방식입니다. 즉, 전체 데이터를 한 번에 처리하지 않고, 일정한 크기로 나누어 처리하는 것이 특징입니다.&lt;/p&gt;
&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;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;Chunk 단위&lt;/b&gt;: 한 번에 처리할 데이터의 크기를 정의합니다. 예를 들어, &lt;span&gt;chunkSize=10&lt;/span&gt;으로 설정하면 10개의 데이터씩 읽고 처리한 후 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;Chunk 기반 처리 흐름&lt;/b&gt;: 데이터를 읽고 &amp;rarr; 처리하고 &amp;rarr; 일정량이 모이면 &amp;rarr; 한 번에 기록(Write)하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tasklet과 Chunk의 차이&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;Tasklet&lt;/b&gt;: 하나의 큰 작업을 단순하게 정의한 단위입니다. 예를 들어, 파일 읽기, 데이터 처리, DB에 쓰기 등의 작업을 순차적으로 한 번에 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;Chunk&lt;/b&gt;: Tasklet과 달리 데이터를 일정한 크기로 나누어 처리할 수 있으며, 대용량 데이터를 처리하는 데 적합합니다. 처리 중 에러가 발생해도, 특정 Chunk만 재처리할 수 있어 유연하고 효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 Chunk로 작업을 구성해야 하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;1.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;효율적인 메모리 관리&lt;/b&gt;: 대용량 데이터를 한 번에 처리하면 메모리 부족 문제가 발생할 수 있습니다. Chunk는 데이터를 나누어 처리하기 때문에 메모리 사용량을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;2.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;재처리 용이&lt;/b&gt;: 처리 중간에 오류가 발생하면, 전체 작업을 다시 수행하는 대신 해당 Chunk만 다시 처리할 수 있어 효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;3.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;성능 최적화&lt;/b&gt;: Chunk는 일정 단위로 데이터를 일괄 저장하므로, I/O 작업 횟수를 줄여 성능을 향상시킬 수 있습니다. 예를 들어, 매번 데이터베이스에 쓰는 대신 Chunk 단위로 데이터를 일괄 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;4.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;대용량 데이터 처리&lt;/b&gt;: 대규모 배치 작업에서 전체 데이터를 한 번에 처리하기 어려운 경우, Chunk는 대량 데이터를 나누어 처리할 수 있어 더 안정적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로, &lt;b&gt;Tasklet&lt;/b&gt;보다 &lt;b&gt;Chunk&lt;/b&gt; 기반 작업 구성이 대량 데이터 처리나 복잡한 배치 처리에서 더 유리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론&lt;/b&gt;: Tasklet은 단순한 작업 처리에 적합하지만, 대량 데이터 처리에서는 메모리 사용과 에러 처리의 효율성을 위해 Chunk 기반 처리가 더 권장됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.Tasklet 구현체 생성하기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GreetingTasklet.java 파일을 생성하고 다음과 같이 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1729430285273&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;

@Slf4j
public class GreetingTask implements Tasklet, InitializingBean {
	@Override
	public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
		log.info(&quot;------------------ Task Execute -----------------&quot;);
		log.info(&quot;GreetingTask: {}, {}&quot;, contribution, chunkContext);

		return RepeatStatus.FINISHED;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		log.info(&quot;----------------- After Properites Sets() --------------&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보는바와 같이 Tasklet과 InitializeBean 인터페이스를 구현한다.&lt;/li&gt;
&lt;li&gt;Tasklet은 execute 메소드를 구현해야한다.&lt;/li&gt;
&lt;li&gt;InitializeBean은 afterPropertiesSet 메소드를 구현해야한다.&lt;/li&gt;
&lt;li&gt;execute:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;execute메소드는 StepContributioin 과 ChunkContext 를 파라미터로 받는다.&lt;/li&gt;
&lt;li&gt;최종적으로 RepeatStatus 를 반환하며 이 값은 다음과 같다.
&lt;ul style=&quot;list-style-type: disc; color: #222222;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FINISHED: 태스크릿이 종료되었음을 나타낸다.&lt;/li&gt;
&lt;li&gt;CONTINUABLE: 계속해서 태스크를 수행하도록한다.&lt;/li&gt;
&lt;li&gt;continueIf(condition): 조건에 따라 종료할지 지속할지 결정하는 메소드에 따라 종료/지속을 결정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;afterPropertiesSet:
&lt;ul style=&quot;list-style-type: disc; color: #222222;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Job, Step 을 생성하고 빈에 등록&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729430369308&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.schooldevops.springbatch.batchsample.config;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

import com.schooldevops.springbatch.batchsample.GreetingTask;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class BasicTaskJobConfiguration {

	private final PlatformTransactionManager transactionManager;

	@Bean
	public Tasklet greetingTasklet() {
		return new GreetingTask();
	}

	@Bean
	public Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
		log.info(&quot;------------------ Init myStep -----------------&quot;);

		return new StepBuilder(&quot;myStep&quot;, jobRepository).tasklet(greetingTasklet(), transactionManager)
													   .build();
	}

	@Bean
	public Job myJob(Step step, JobRepository jobRepository) {
		log.info(&quot;------------------ Init myJob -----------------&quot;);
		return new JobBuilder(&quot;myJob&quot;, jobRepository).incrementer(new RunIdIncrementer())
													 .start(step)
													 .build();
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tasklet, step, Job을 구현하고 Bean으로 등록한다.&lt;/li&gt;
&lt;li&gt;step 구성 시 PlatformTransactionManager와 jopRepository를 파라미터로 받는다.&lt;/li&gt;
&lt;li&gt;Job 구성 시 시작 점 구성을 위해 step을 파라미터로 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 실행결과&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-20 오후 10.23.31.png&quot; data-origin-width=&quot;2738&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQixTy/btsKcSpTmci/z32Vcxunyh0J0sSTQ3Cuk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQixTy/btsKcSpTmci/z32Vcxunyh0J0sSTQ3Cuk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQixTy/btsKcSpTmci/z32Vcxunyh0J0sSTQ3Cuk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQixTy%2FbtsKcSpTmci%2Fz32Vcxunyh0J0sSTQ3Cuk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2738&quot; height=&quot;758&quot; data-filename=&quot;스크린샷 2024-10-20 오후 10.23.31.png&quot; data-origin-width=&quot;2738&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그를 보면 Spring Batch에서 &lt;span&gt;myStep&lt;/span&gt;의 로그가 &lt;span&gt;myJob&lt;/span&gt;의 로그보다 먼저 출력되었다. 그이유는 무엇일까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Bean 초기화 순서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 빈은 &lt;b&gt;의존성 주입과 초기화&lt;/b&gt; 순서에 따라 설정됩니다. &lt;span&gt;myJob&lt;/span&gt;은 &lt;span&gt;myStep&lt;/span&gt;을 의존하므로, &lt;span&gt;myStep&lt;/span&gt;&lt;b&gt;이 먼저 생성&lt;/b&gt;되고 초기화된 후에 &lt;span&gt;myJob&lt;/span&gt;이 초기화됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 &lt;span&gt;@Bean&lt;/span&gt;으로 정의된 &lt;span&gt;step()&lt;/span&gt; 메서드가 먼저 호출되어 &lt;span&gt;myStep&lt;/span&gt;을 초기화하고, 그 후 &lt;span&gt;myJob()&lt;/span&gt; 메서드가 호출되어 &lt;span&gt;myJob&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;b&gt;2. 코드 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;span&gt;myJob&lt;/span&gt; 빈을 생성할 때 &lt;span&gt;step&lt;/span&gt;이 필요하므로, Spring은 먼저 &lt;span&gt;step&lt;/span&gt; 빈을 초기화하고, 해당 단계에서 로그가 찍힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;step()&lt;span&gt; 메서드에서 &lt;/span&gt;greetingTasklet()&lt;span&gt;과 &lt;/span&gt;transactionManager&lt;span&gt;가 호출되면서 &lt;/span&gt;myStep&lt;span&gt;이 초기화되고, 로그가 출력됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;그 이후 &lt;span&gt;myJob&lt;/span&gt;이 생성되며 &lt;span&gt;myJob&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;b&gt;결론&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;myJob&lt;/span&gt;은 &lt;span&gt;step&lt;/span&gt;을 의존하기 때문에, Spring이 의존 관계에 맞춰 &lt;span&gt;step&lt;/span&gt; 빈을 먼저 초기화하면서 해당 로그가 먼저 찍힌다.&lt;/p&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/8</guid>
      <comments>https://j0free.tistory.com/8#entry8comment</comments>
      <pubDate>Sun, 20 Oct 2024 22:27:15 +0900</pubDate>
    </item>
    <item>
      <title>1.SpringBatch 빠르게 시작하기</title>
      <link>https://j0free.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이스북에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;S&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;&lt;b&gt;pringBatch&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;스터디 모집한다는 글을 보았고, 신청해서 참여하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;스터디 진행 방식은&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;Devocean 블로그&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;pringBatch 연재 시리즈를 보고 실습을 통해,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;S&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;pringBatch&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;의 기초를 학습하고 서로 논의를 하며 공부하는 스터디입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;앞으로 스터디가 진행되는 동안 공부한 내용을 정리하고자 합니다.&lt;br /&gt;틀리거나 보충 할 내용이 있다면 댓글 부탁드립니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고 :&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=166164&quot;&gt;&lt;b&gt;[SpringBatch 연재 01] SpringBatch 빠르게 시작하기&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. S&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;pringBatch 프로젝트 구성&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이를 통해 프로젝트 이름 및 JDK 17로 프로젝트 생성 후 build.gradle에 필요한 의존성을 설정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729411179377&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.2'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.schooldevops.spring-batch'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    runtimeOnly 'com.h2database:h2'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.batch:spring-batch-test'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBatch 프로젝트를 구성하기 위한 필수 의존성&lt;/p&gt;
&lt;div style=&quot;background-color: #263238; color: #c3cee3;&quot;&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-batch'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2가 runtime시에 필요하기 때문에 rumtimeOnly로 h2 의존성을 추가&lt;/p&gt;
&lt;div style=&quot;background-color: #263238; color: #c3cee3;&quot;&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;runtimeOnly 'com.h2database:h2'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. h2 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729411612174&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# H2 DataBase용
spring:
  batch:
    jdbc:
      initialize-schema: always
    job:
      enabled: true
  datasource:
    driver-class-name: org.h2.Driver  # Database를 H2로 사용하겠다.
    url: jdbc:h2:~/test  # H2 접속 정보
    username: sa  # H2 접속 시 입력할 username 정보 (원하는 것으로 입력)
    password:  # H2 접속 시 입력할 password 정보 (원하는 것으로 입력)
    hikari:
      maximum-pool-size: 10
  h2:
    console: # H2 DB를 웹에서 관리할 수 있는 기능
      enabled: true           # H2 Console 사용 여부
      path: /h2-console       # H2 Console 접속 주소
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;SpringBatch &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실행 시 DB 설정을 해주지 않으면, DataSoruce 설정이 필요하다고 에러가 나오며 실행이 되지 않을 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;빠른 구성을 위해 h2 embedded 설정해서 DataSource 설정해보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;접속 URL : http://localhost:8080/h2-console&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-20 오후 5.10.12.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/spF4h/btsKdezfSvf/WaLuDA6wFnsZqGbAJ4WIk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/spF4h/btsKdezfSvf/WaLuDA6wFnsZqGbAJ4WIk1/img.png&quot; data-alt=&quot;h2 접속화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/spF4h/btsKdezfSvf/WaLuDA6wFnsZqGbAJ4WIk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FspF4h%2FbtsKdezfSvf%2FWaLuDA6wFnsZqGbAJ4WIk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;574&quot; height=&quot;389&quot; data-filename=&quot;스크린샷 2024-10-20 오후 5.10.12.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;h2 접속화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2에 접속하게 되면 위와 같은 화면이 나올 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yaml에 설정해 놓은 접속정보를 통해 접속을 해봅시다.&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-filename=&quot;스크린샷 2024-10-20 오후 5.11.25.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9DjRO/btsKb0ovCvC/kKvx4bgFuiuveK9EJ6t7Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9DjRO/btsKb0ovCvC/kKvx4bgFuiuveK9EJ6t7Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9DjRO/btsKb0ovCvC/kKvx4bgFuiuveK9EJ6t7Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9DjRO%2FbtsKb0ovCvC%2FkKvx4bgFuiuveK9EJ6t7Nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;412&quot; data-filename=&quot;스크린샷 2024-10-20 오후 5.11.25.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBatch에 필요한 기본 테이블이 생성된 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729411946694&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  batch:
    jdbc:
      initialize-schema: always&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 schema: always로 설정 했기 때문에 서버가 구동 시 기본 테이블을 매번 새로 생성하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;자동 생성 시 spring.batch.jdbc.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;initialize-schema 설정은 ALWAYS, EMBEDDED, NEVER로 설정이 가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;b&gt;ALWAYS&lt;/b&gt; : &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;스크립트를 항상 실행합니다. RDBMS 설정이 되어 있을 경우 내장 DB보다 우선적으로 실행됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;b&gt;EMBEDDED&lt;/b&gt; : &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;내장 DB일 때만 실행되며 스키마가 자동 생성됩니다. (기본값)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;b&gt;NEVER&lt;/b&gt; : &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;스크립트를 항상 실행하지 않습니다. 운영에서 수동으로 스크립트 생성 후 설정하는 것을 권장합니다. 내장 DB일 경우 스크립트가 생성이 안되기 때문에 오류가 발생합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;그래도 접속이 안된다면, &lt;b&gt;@EnableBatchProcessing&amp;nbsp;&lt;/b&gt;어노테이션을 붙였는지 확인 해보도록 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Spring Boot 3.0에서는 '@EnableBatchProcessing' 사용이 권장되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 이 어노테이션이 Spring Batch의 자동 구성을 활성화하는 데 사용되었지만, 이제는 더 이상 필요하지 않아 어플리케이션에서 제거해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;'@EnableBatchProcessing'을 추가하면 Spring Batch의 자동 구성(메타데이터 테이블 생성, 작업 시작 시 자동 실행 등)이 비활성화되기 때문입니다.&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 스프링배치 스키마 구조&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링배치를 수행하면 자동으로 배치를 위한 스키마가 생성이 된다.&lt;/li&gt;
&lt;li&gt;다음은 스키마 구조이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo40W5/btsKdfEUJWj/8NomRUo8x5WOUdnolxKp5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo40W5/btsKdfEUJWj/8NomRUo8x5WOUdnolxKp5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo40W5/btsKdfEUJWj/8NomRUo8x5WOUdnolxKp5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo40W5%2FbtsKdfEUJWj%2F8NomRUo8x5WOUdnolxKp5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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-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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch의 메타데이터 스키마는 배치 작업의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실행 기록을 추적&lt;/b&gt;하고, 재시작 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;상태를 관리&lt;/b&gt;하는 데 사용된다. 스키마는 주로 다음과 같은 주요 테이블들로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/schema-appendix.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-batch/reference/schema-appendix.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1729414238562&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Meta-Data Schema :: Spring Batch&quot; data-og-description=&quot;The Spring Batch Metadata tables closely match the domain objects that represent them in Java. For example, JobInstance, JobExecution, JobParameters, and StepExecution map to BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_JOB_EXECUTION_PARAMS, and BATCH_ST&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-batch/reference/schema-appendix.html&quot; data-og-url=&quot;https://docs.spring.io/spring-batch/reference/schema-appendix.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bASUoB/hyXhUWvo1g/fku8ShYwdKQvKOt08nm0lk/img.png?width=902&amp;amp;height=746&amp;amp;face=0_0_902_746&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/schema-appendix.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-batch/reference/schema-appendix.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bASUoB/hyXhUWvo1g/fku8ShYwdKQvKOt08nm0lk/img.png?width=902&amp;amp;height=746&amp;amp;face=0_0_902_746');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Meta-Data Schema :: Spring Batch&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Spring Batch Metadata tables closely match the domain objects that represent them in Java. For example, JobInstance, JobExecution, JobParameters, and StepExecution map to BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_JOB_EXECUTION_PARAMS, and BATCH_ST&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Spring Batch</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/7</guid>
      <comments>https://j0free.tistory.com/7#entry7comment</comments>
      <pubDate>Sun, 20 Oct 2024 17:49:12 +0900</pubDate>
    </item>
    <item>
      <title>인프라 공방 - 1.AWS 수동 키 생성</title>
      <link>https://j0free.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에 접속하기 위해서는 pem key를 생성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 및 보안 -&amp;gt; 키페어 -&amp;gt; 키 페어 생성을 순서대로 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-24 오후 9.25.59.png&quot; data-origin-width=&quot;3784&quot; data-origin-height=&quot;1300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdZxwh/btrPujiaKDr/3Ktd80V5pyeqxN9a1u3qGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdZxwh/btrPujiaKDr/3Ktd80V5pyeqxN9a1u3qGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdZxwh/btrPujiaKDr/3Ktd80V5pyeqxN9a1u3qGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdZxwh%2FbtrPujiaKDr%2F3Ktd80V5pyeqxN9a1u3qGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3784&quot; height=&quot;1300&quot; data-filename=&quot;스크린샷 2022-10-24 오후 9.25.59.png&quot; data-origin-width=&quot;3784&quot; data-origin-height=&quot;1300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key 이름 및 파일 형식을 선택 -&amp;gt; 키 페어 생성 버튼을 누른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-24 오후 9.31.20.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvqYVP/btrPsAyrJpv/tq1p48yrngNTIXyHur8Vf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvqYVP/btrPsAyrJpv/tq1p48yrngNTIXyHur8Vf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvqYVP/btrPsAyrJpv/tq1p48yrngNTIXyHur8Vf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvqYVP%2FbtrPsAyrJpv%2Ftq1p48yrngNTIXyHur8Vf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1638&quot; height=&quot;1460&quot; data-filename=&quot;스크린샷 2022-10-24 오후 9.31.20.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>교육</category>
      <category>AWS</category>
      <category>pem key</category>
      <author>j0free</author>
      <guid isPermaLink="true">https://j0free.tistory.com/6</guid>
      <comments>https://j0free.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 24 Oct 2022 21:33:04 +0900</pubDate>
    </item>
  </channel>
</rss>