개요

2분 마다 시총 상위 100개 코인에 대한 가격정보를 크롤링하려 한다.
크롤링을 위해서는 Batch Job 과 Scheduler가 필요한데, 어떤 환경으로 구성할까 고민하다 Batch Job은 Spring Batch로 Scheduler는 Jekins로 구성했다.

Spring Batch를 쓴 이유

1. Spring boot를 사용중이라 기존에 만들어둔 Service를 재사용할 수 있다.
2. Transaction, Datasouce 관리가 편하다.
3. 익숙한 환경 (Java, RestTemplate, Error handling 등등)

Jekins를 쓴 이유

1. GUI 환경 Batch Job 관리가 편리하다. (상태확인, 실행 주기 관리, 로그 확인 등등)
2. Jenkins에서 Jar 형태의 Batch Job Application 을 직접 실행할 경우, 로그 확인이 편리

문제점

Spring Batch 를 단순 크롤링 작업으로만 사용하기에는 오버스팩!
오버스팩인것은 맞다. 하지만 구현 속도는 가장 빠르다.

Q Spring Batch 는 Database Table로 Job을 관리하는데 Jekins로 실행한다고?
A Database Table를 이용하지 않고, In - Memory 에서 작동하도록 설정했다. 

Q Batch Application를 이중화 하려면? 
A 이중화가 불가능한 문제다. 다만, 배포를 위한 중단시간이 제로에 가깝고, 잡을 분리하는 등 운영방식으로 해결할 수 있다.

구현

1. 스프링 배치 설정

Spring Batch 의존성 추가 (Spring boot 2.1 기준)

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>

Batch Config, Batch Job 생성 
(InMemoryBatchConfigurer는 따로 생성했다.)

@Configuration
@EnableBatchProcessing
public class BatchJobConfig {

@Autowired
JobBuilderFactory jobBuilderFactory;
@Autowired
StepBuilderFactory stepBuilderFactory;

@Bean
public BatchConfigurer batchConfigurer() {
return new InMemoryBatchConfigurer();
}

@Autowired
private UpdateTop100CoinPriceTasklet updateTop100CoinPriceTasklet;

@Bean
public PlatformTransactionManager transactionManager() {
return new ResourcelessTransactionManager();
}

@Bean
public Job updateTop100CoinPriceJob() {
return jobBuilderFactory.get("updateTop100CoinPrice")
.start(updateTop100CoinPrice())
.build();
}

@Bean
public Step updateTop100CoinPrice() {
return stepBuilderFactory.get("updateTop100CoinPrice")
.tasklet(updateTop100CoinPriceTasklet)
.build();
}
}

Batch Tasklet 생성
Tasklet과 InitializingBean을 구현하면 된고 RepeatStatus를 통해 Tasklet 반복여부를 선택할 수 있다.
Tasklet에서 실행시킬 로직은 기존 Service Bean 주입을 통해 해결할 수 있어서 편

@Component
public class UpdateTop100CoinPriceTasklet implements Tasklet, InitializingBean {

@Autowired
MarketCapService marketCapService;

@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
marketCapService.updateTop100CoinPrice();
return RepeatStatus.FINISHED;
}

@Override
public void afterPropertiesSet() throws Exception {

}
}

In - Memory Batch Configurer 설정
Database Table로 Job을 관리하면 여러모로 신경써야할게 많다. 단순한 목적에 맞게 Memory에서 관리하자. 

public class InMemoryBatchConfigurer implements BatchConfigurer {

private PlatformTransactionManager transactionManager;
private JobRepository jobRepository;
private JobLauncher jobLauncher;
private JobExplorer jobExplorer;

@Override
public PlatformTransactionManager getTransactionManager() {
return transactionManager;
}

@Override
public JobRepository getJobRepository() {
return jobRepository;
}

@Override
public JobLauncher getJobLauncher() {
return jobLauncher;
}

@Override
public JobExplorer getJobExplorer() {
return jobExplorer;
}

@PostConstruct
public void initialize() {
if (this.transactionManager == null) {
this.transactionManager = new ResourcelessTransactionManager();
}
try {
MapJobRepositoryFactoryBean jobRepositoryFactoryBean =
new MapJobRepositoryFactoryBean(this.transactionManager);
jobRepositoryFactoryBean.afterPropertiesSet();
this.jobRepository = jobRepositoryFactoryBean.getObject();

MapJobExplorerFactoryBean jobExplorerFactoryBean =
new MapJobExplorerFactoryBean(jobRepositoryFactoryBean);
jobExplorerFactoryBean.afterPropertiesSet();
this.jobExplorer = jobExplorerFactoryBean.getObject();

SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.afterPropertiesSet();
this.jobLauncher = jobLauncher;
} catch (Exception e) {
throw new BatchConfigurationException(e);
}
}
}

2. Application 실행환경 설정

Application Type을 WEB에서 NONE으로 변경했다. 이제 포트를 찾지 않아도 되어서 여러 Job들을 실행할 수 있다. (프로세스 생성비용은 늘어나지만..)
Spring Batch CommandLineJobRunner 를 사용할까 하다가, 어렵고 과한것 같아서 Application 실행 변수로 Batch Job 명을 전달 받아 실행하는 방식으로 세팅했다. 

@Slf4j
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
@EnableAsync
@EnableScheduling
@EnableCaching
@EnableTransactionManagement(proxyTargetClass = true)
@EnableJpaRepositories(basePackages = "com.happy.life")
public class BatchApplication {

public static void main(String[] args)
throws JobParametersInvalidException, JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException {

SpringApplication app = new SpringApplication(BatchApplication.class);
app.setWebApplicationType(WebApplicationType.NONE);
ConfigurableApplicationContext ctx = app.run(args);
JobLauncher jobLauncher = ctx.getBean(JobLauncher.class);

if (args.length > 0) {
Job job = ctx.getBean(args[0], Job.class);
jobLauncher.run(job, new JobParameters());
} else {
log.error("배치 잡 이름을 입력해 주세요. EX) {}", "java -jar app.jar updateTop100CoinPriceJob");
}

System.exit(0);
}
}

3. 젠킨스 설정

젠킨스를 설치하고 깃헙 연동하는 과정은 생략한다.

배포하기

젠킨스에서 Freestyle project를 하나 만들어서 Batch Application을 배포하자.
Github에서 소스를 받아와 maven build하는 기본 빌드 이후에 jar를 복사한다.  yes|cp -rf 옵션을 줘서 항상 덮어쓰기 하도록한다.
이렇게 하면 순단없이 아주 빠르게 배포? 한다고 말할 수 있지않을까? 호호호호

실행하기

배포한 jar를 실행한다. Jenkins의 Build periodically값을 조정해서 2분만다 실행하도록 한다.
그리고 Execute shell 에서 Java -jar 옵션으로 배치를 실행한다.

결과 확인

Job에서 뱉어내는 로그들을 잘 기록하면서 수행된 것을 확인할 수 있다. 오예 끝~!


결론

Spring Batch가 소잡는 칼이라 닭을 더 쉽게 잡을 수 있다.

+ Recent posts