Spring - 38.2 Batch / Scheduling / thread

갓김치·2021년 1월 12일
0

JSP+Spring

목록 보기
43/43

처리

실시간 처리

  • 명령에 대한 처리 결과가 그 즉시 돌아옴

batch 처리

  • 사용자의 인터랙션 없이 컴퓨터에 의해 일련의 프로그램 집합이 처리되는 것을 의미
  • 명령을 일정기간 모아두었다가 특정 조건을 만족하는 시점이 되면 모아놨던 명령을 한꺼번에 처리함
  • Ex) 프린트 출력, 로그 분석, 주기적 처리가 필요한 회계 결산 이나 급여 작업등.

특징

  • 관리자에 의한 실행
  • 스케줄링 사용
    • 특정 시간에 일정 주기에 따라 실행
  • 백그라운드로 실행이 되어야함 = 사용자와의 인터랙션 필요 없음
    • ex: 일주일동안 동일한 아이디로 재가입이 불가능하다
    • 네이버가 가장 한산한 시간대에 탈퇴, 그때 다른 기능에는 영향이 없어야함 = 데몬스레드로 작업해야함

기타

  • 대용량 데이터 처리
  • 근데 최프에 대용량인 조가 없음
  • 그래서 스케줄링으로 대체
  • 스레드와 스케줄링을 오후에 공부할 것
  • 스케줄러 프레임워크 사용

Multithreading

public class PrintNumberJob implements Runnable{

	private int number;

	@Override
	public void run() {
		while(number<101) {
			System.out.printf("%d - %s[active count : %d]\n", ++number, Thread.currentThread().getName(), Thread.activeCount());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}
public class PrintNumberJobGenerator implements Runnable{

	private int jobCount;
	
	@Override
	public void run() {
		while(jobCount++<10) { // 이게 true라면?
			System.out.println((jobCount)+"번째 PrintNumberJob생성");
			PrintNumberJob job = new PrintNumberJob();
			Thread jobThread = new Thread(job); // 자원을 할당받는(=cpu 1개를 선점할 수 있는) 구조는 됐으나 아직 시작은 안됨
			jobThread.start();
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}
public class SimpleThreadTest {
	public static void main(String[] args) {
		PrintNumberJobGenerator generator = new PrintNumberJobGenerator();
//		generator.run(); // run()은 스레드를 분리하지 않음
		Thread generatorThread = new Thread(generator); // 자원을 할당받는(=cpu 1개를 선점할 수 있는) 구조는 됐으나 아직 시작은 안됨
		generatorThread.start(); // 나 이제부터 cpu필요해! 대기순번으로 들어감
		System.out.println("메인 쓰레드 end");
	}
}

문제점
1. 자원이 한정된 상태에서 무한반복을 돌면? => 풀링으로 해결
2. job과 schedule이 분리되어있찌 않음 => 스케줄러로 해결

  • 톰캣의 확장cgi방식..

Java만을 이용한 스케줄링

  • Timer, Timer Task api
  • thread pooling
  • SchedulerLab의 simple, pooling 패키지



Spring 스케줄링

SchduleLab

1. 스케줄링 할 작업 생성 (PrintNumberJob)

2. task-context.xml

task:scheduler

  • 스케줄링: 매1초마다 작업 (java core의 timer)
  • poolsize: 타이머(스케줄러)도 여러개 운영할 수 있는 구조이다

task:executor

  • 풀링: 일정갯수의 스레드만 만들어서 운영해야함 (java core의 ThreadPoolExecutor)

task:annotation-driven

  • <task:annotation-driven scheduler="scheduler" executor="executor"/>
  • 선언적 프로그래밍을 가능하게 해줌

3. 작업에 어노테이션 부여

  • @Component
  • @Scheduled(initialDelay=0,fixedDelay=1000)

fixed rate vs. fixed delay

fixed ratefixed delay

cron 표현식

  • fixed로 해결할 수 없는 다양한 설정
•"0 0 * * * *" = the top of every hour of every day.
•"*/10 * * * * *" = every ten seconds.
•"0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day.
•"0 0 6,19 * * *" = 6:00 AM and 7:00 PM every day.
•"0 0/30 8-10 * * *" = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
•"0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays
•"0 0 0 25 12 ?" = every Christmas Day at midnight

4. 테스트

  • 테스트여서 테스트 이후 컨테이너가 닫혀 가비지컬렉션되어 작업이 멈추는 문제때문에 일반패키지에서 테스트

5. SpringTaskTestView.java

public class SpringTaskTestView {
	public static void main(String[] args) {
		ConfigurableApplicationContext container =
				new ClassPathXmlApplicationContext("kr/or/ddit/springtask/conf/task-context.xml");
		container.registerShutdownHook();
		
		// 실행하면 아무것도 주입받고 실행한적없지만 등록되어있는 task에 따라 작업이 진행됨
	}
}

  • active count가 6을 넘어가지않음

Quartz 스케줄링

기본 사용 방법

  • 매뉴얼북의 34.6 Using the Quartz Scheduler 참조
  • SchedulerLab의 quartz 패키지

1. 스프링과의 연동

<!-- tx 모듈도 필요 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-tx</artifactId>
  <version>4.3.29.RELEASE</version>
</dependency>
<!-- quartz와의 연동, 메일 수발신 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-support</artifactId>
  <version>4.3.29.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.3.0</version>
</dependency>
  • org.opensymphony.quartz
    • Spring 3버전에서는 Quartz 1버전과 연동해야한다. 이때는 이 디펜던시 사용

2. PrintNumberQuartzJobBean.java

public class PrintNumberQuartzJobBean extends QuartzJobBean {

	private int number;
	
	/*
	 * 작업정의
	 */
	@Override
	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
		System.out.printf("%d - %s[active count : %d]\n"
				, ++number, Thread.currentThread().getName(), Thread.activeCount());
	}

}
  • 작업을 정의한다

3. quartz-context.xml 정의

<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"
      p:jobClass="kr.or.ddit.quartz.PrintNumberQuartzJobBean"
      >
  <property name="jobDataAsMap">
    <map>
      <entry key="today">
        <bean class="java.util.Date" />
      </entry>
    </map>
  </property>
</bean>
  • factoryBean 등록시 factoryBean이 bean으로 등록되는게 아니라 factoryBean으로 생성되는 것이 bean으로 등록되는것
    • 여기에선 jobDetail이 bean으로 등록되는 것
  • PNQJB의 한계점
    • 스프링컨테이너 안에있지않고 quartz가 관리하는 녀석
    • spring bean을 inject받을 수 없다

4. Wiring up job using trigger and the SchedulerFactoryBean

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean"
      p:jobDetail-ref="jobDetail"
      p:startDelay="0"
      p:repeatInterval="1000"
      p:repeatCount="10"
      />
  • SimpleTrigger(fixedrate,...) vs CronTrigger
  • repeatCount(작업횟수제한) vs repeatInterval(=fixedrate)
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
  <property name="triggers">
    <array>
      <ref bean="simpleTrigger"/>
    </array>
  </property>
</bean>
  • trigger가 동작할 수 있도록 백그라운드 컨텍스트를 만들어주는게 scheduler

5. QuartzSchedulerTestView

  • 이런 문제점 발생
    • PNQJB가 spring에서 관리되는게 아닌 quartz에서 관리되고있음
    • quarz-context.xml: 이건어떤문제점이었지
    • extends QuarzJobBean : quartz없이 운영할수 없는 quartz종속적인 non-pojo객체가되버림

6. bean 재등록

<!-- PNJ를 spring bean으로 등록, 하나의 작업이 하나의 메서드 단위로 묶여야함-->
<bean id="printNumberJob" class="kr.or.ddit.quartz.PrintNumberJob" />
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
      p:targetObject-ref="printNumberJob"
      p:targetMethod="printNumber"
      />

작업이 끝나면 알림주는 방법

  • quartz의 장점: 모니터링 코드 넣는게 쉬워짐

1. listener 구현

public class CustomJobListener extends JobListenerSupport{

	@Override
	public String getName() {
		return this.getClass().getSimpleName();
	}
	
	@Override
	public void jobExecutionVetoed(JobExecutionContext context) {
		System.err.println("작업 거절");
	}
	
	@Override
	public void jobToBeExecuted(JobExecutionContext context) {
		System.err.println("작업 실행");
	}
	
	@Override
	public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
		System.err.println("작업 완료");
	}
}

2. listener 등록

<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
  <property name="globalJobListeners">
    <array>
      <bean class="kr.or.ddit.quartz.CustomJobListener" />
    </array>
  </property>
  <property name="triggers">
    <array>
      <ref bean="simpleTrigger"/>
    </array>
  </property>
</bean>

3. 실행




Quartz이용해 탈퇴처리

  • 원래는 db에 멤버관련 자료를 지우고 delete가 되는 프로시져가 있어야하지만 그정도로까지 하진 않을것
  • 월요일 새벽3시에 탈퇴시키는 작업
  • mem_delete = y인애 조회
  • for문을 돌며 자식들을 지움(ex: 카트)
  • 그 후 member를 삭제
  • 절차적인 쿼리를 하나로 묶으려면 procedure(return값 x)이나 function(return값o)으로 묶어야함
    • function 실행하려면 쿼리구문 안에서 실행해야함
    • 그래서 보통 procedure로 사용
  • mybatis 모듈을 서비스로 끌고와 호출?

1. service패키지에 MemberDeleteJob 작업정의

2. container 등록

<!-- targetObject bean은 MemberDeleteJob.java에서 어노테이션으로 등록됨, coc에 의한 id를 ref로 넣어줌 -->
<bean id="methodInvokingJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
      p:targetObject-ref="memberDeleteJob"
      p:targetMethod="deleteMember"
      />
<!-- 월요일 새벽 3시(0 0 3 ? * MON)라는 특정 조건을 위해 CronTrigger가 필요 -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"
      p:jobDetail-ref="methodInvokingJobDetail"
      p:cronExpression="0/5 * * * * ?"
      />

<!-- jobDetail과 trigger가 놀 수 있는 context -->
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"
      p:triggers-ref="cronTrigger"
      />
  • 대부분의 batch작업은 상위 컨테이너에서 많이 처리
    • batch는 웹종속성이 없기때문
    • 하위여도 상관없긴한데 상위에 많이함



Spring Batch

  • https://spring.io/guides/gs/batch-processing/
  • 해당 예제는 java config + spring boot 로 되어있음
  • 우리는 기존에 해오던대로 xml config + spring framework 방식으로 써보려고함
  • 사용할때: 일괄적 대용량 처리
    • 한전에서 한달에 한번씩 1000만명의 사람에게 고지서 발급
    • 요금계산이 이루어져야함
    • 계량기에 정보로 요금산정
    • 매달 20일에 자동 고지서 발급
    • 0시 0분 0초에 천만명의 가입자의 요금을 먼저 계산해야함
    • 계산된 요금에 따라 고지서 발부 작업이 이루어져야함
    • 20일에 고지서 발부하는 자겁이 한번에 뚝 끝나는게 아니고 단계적으로 진행이 되어야함
    • 하나의 배치작업이 단계적인 작업을 쪼개질때 전체 배치작업을 job이라고 하고 소단위 작업을 step이라고 표현
    • job: 고지서 발급
    • job과 step의 관계: association
    • step: 하나의 job안에 여러step(1:다), 하나의 step안에 1:1로 Reader, Processor, Writer가 이루어져있음
      • 1번스텝
      • Reader: 요금계산을 위해 batch값을 읽어오기위함
      • Processor: 단위 요금 계산 로직이 돌아가야함
      • Writer: 계산 로직으로 얻어낸 최종 요금을 어딘가에 저장해놔야함
      • 2번스텝
      • reader: 요금 읽어와
      • processor: 폼만들어
      • writer: 출력하거나, db에넣거나 하는 작업
  • batch는 background로 multithreading활용
    • jobLauncher: daemon thread형태로 job실행해주는 애
      • 1000만명을 한번에 할 수 없으니 나눠서 처리, 나누는 묶음 단위가 chunck
      • 만명씩 chunck -> 1000번의 작업 -> 100번작업하고 정전났다면? -> 이미 처리된 100번작업은 어떡하지? 날려버리면 전기들어오면 처음부터 다시해야되는데? -> 현재 어느chunck까지 작업이 됐고 어느정도 됐는지 저장을 job repository에 해야함

1. dependency 추가

  • org.springframework.batch의 core 모듈
  • 최신은 4.3이나 spring 5.대와 연동되는 녀석이기때문에 3.0.10버전 사용

2. csv 예제파일 및 sql파일

  • 5명의 이름을 대문자로 바꾸어 hsqldb라는 embeded db에 넣을 예정
<dependency>
  <groupId>org.hsqldb</groupId>
  <artifactId>hsqldb</artifactId>
  <scope>runtime</scope>
</dependency>
  • com.example.batchprocessing 라는 패키지, 폴더구조를 java와 resource에 각각 생성

3. datasource-context.xml

  • hsqldb를 위한 datasource 읽어들이는 컨텍스트 필요
<jdbc:embedded-database type="HSQL" id="realDataSource">
  <jdbc:script location="classpath:schema-all.sql"/>
</jdbc:embedded-database>

<!-- 어플리케이션이 읽기 위해 id는 반드시 dataSource로 -->
<bean class="net.sf.log4jdbc.Log4jdbcProxyDataSource"
      c:realDataSource-ref="realDataSource"
      />

4. person class (Create a Business Class)

5. comma seperate volume

  • 한줄한줄읽고 comma를 토큰으로 쪼개야함
  • io 작업에 파싱하고 데이터 읽어 vo도 만들어야하지.. 너무 복잡 -> spring batch에 이 기능있음

6. batch-context.xml에 reader,processor,writer 등록

reader

<bean id="flatFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader"
      p:resource="classpath:sample-data.csv"
      p:name="personItemReader"
      >
  <property name="lineMapper"><!-- 한줄씩 처리 -->
    <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
      <property name="lineTokenizer"><!-- names로 토큰들이 식별자를 갖게됨 -->
        <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer" 
              p:delimiter=","
              p:names="firstName,lastName"
              />
      </property>
      <property name="fieldSetMapper"><!-- Person에 매핑, line에서 토큰의 순서와 필드 정보를 매핑 -->
        <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
              p:targetType="com.example.batchprocessing.Person"
              />
      </property>
    </bean>
  </property>
</bean>

7. junit으로 테스트

8. step들을 job으로 묶어주기

<batch:job id="personJob">
  <batch:step id="step1">
    <batch:tasklet>
      <!-- commit-interval: chunk사이즈 == writer 인자 list의 사이즈 -->
      <batch:chunk
                   commit-interval="10"
                   reader="flatFileItemReader"
                   processor="personItemProcessor"
                   writer="jdbcBatchItemWriter"
                   />
    </batch:tasklet>
  </batch:step>
</batch:job>

9. 작업 실행자

datasource-context

<jdbc:embedded-database type="HSQL" id="realDataSource">
  <jdbc:script location="classpath:schema-all.sql"/>
  <jdbc:script location="classpath:/org/springframework/batch/core/schema-drop-hsqldb.sql"/><!--이거새로추가-->
</jdbc:embedded-database>

batch-context

<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
      p:dataSource-ref="dataSource"
      />
<!-- 작업 실행자  (p:taskExecutor로 thread pooling정책도 설정 가능)-->
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher"
      p:jobRepository-ref="jobRepository"
      />

10. BatchContextTestView 에서 테스트

public class BatchContextTestView {
	public static void main(String[] args) throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException {
		ConfigurableApplicationContext container
			= new ClassPathXmlApplicationContext("com/example/batchprocessing/conf/*-context.xml");
		container.registerShutdownHook();
		
		JobLauncher jobLauncher = container.getBean(JobLauncher.class);
		Job personJob = container.getBean("personJob", Job.class);
		JobParameters jobParameters = new JobParametersBuilder()
										.addDate("today", new Date())
										.toJobParameters(); // toJobParameter == build
		jobLauncher.run(personJob, jobParameters);
	}
}
  • 예외발생
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'step1': Cannot resolve reference to bean 'jobRepository' while setting bean property 'jobRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jobRepository' defined in file [D:\A_TeachingMaterial\8.gitRepository\lec-202007\SchedulerLab\target\classes\com\example\batchprocessing\conf\batch-context.xml]: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: TransactionManager must not be null.
  • 청크단위로 커밋 인터벌하기때문에 트랜잭션 관리가 필요하기때문에

예외해결

datasource

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
      p:dataSource-ref="dataSource"
      />

batch

<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
      p:dataSource-ref="dataSource"
      p:transactionManager-ref="transactionManager"
      />

다른 예외 발생

Caused by: java.sql.SQLSyntaxErrorException: user lacks privilege or object not found: BATCH_JOB_INSTANCE in statement [SELECT JOB_INSTANCE_ID, JOB_NAME from BATCH_JOB_INSTANCE where JOB_NAME = ? and JOB_KEY = ?]

Caused by: org.hsqldb.HsqlException: user lacks privilege or object not found: BATCH_JOB_INSTANCE
  • drop 스키마를 가져가서 오류발생함
  • 라이브러리에서 기본 스키마로 변경

datasource

<jdbc:embedded-database type="HSQL" id="realDataSource">
  <jdbc:script location="classpath:schema-all.sql"/>
  <jdbc:script location="classpath:/org/springframework/batch/core/schema-hsqldb.sql"/>
</jdbc:embedded-database>

테스트 다시 실행

  • 5번 실행되고 있음

11. 쿼츠로 스케줄링까지 해결

  • quartz-context.xml

12. 너무 많은 로그 처리

  • JobCompletionNotificationListener.java
profile
갈 길이 멀다

0개의 댓글