Programing/OpenSource

[JobRunr] 시작하기

나모찾기 2024. 5. 5. 17:28

요약

1. https://start.spring.io/ 에서 의존성은 Spring Web만 선택하여 생성

2. JobRunr 의존성 추가

3. Spring Data JPA, H2 의존성 추가

4. JobRunr 대시보드 켜기

5. 지연된 작업 수행 수행


진행 배경

주기적으로 수행하는 작업이나 특정 시간을 두고 동작하는 작업은 스프링 스케쥴러이나 스프링 배치로 구현이 가능하다.

하지만 스프링 배치는 배치 처리 작업을 지원하는 강력한 프레임워크라는 장점이 있지만, 초기 테이블 구성 등의 다양한 설정이 필요하고 배치 작업을 처음 접하는 개발자에게 학습 곡선이 있다는 단점도 있다.

반면 스프링 스케쥴러는 기존 환경에 간단하고 쉽게 구성을 할 수 있다는 장점이 있는 반면, 2개 이상의 인스턴스가 있는 분산 환경에서 적용할 경우 동일한 작업(Job)이 동시에 수행되는 일이 발생할 수 있다. 이렇게 되면 한 번에 하나만 돌아야 하는 작업이 중복 처리되어 예상하지 못한 부수효과(Side-effect)가 발생하는 단점이 있다.

 

JobRunr은 이런 분산 자바 환경을 염두해두고 만든 백그라운드 작업 스케줄 처리기이다.

The ultimate library for background processing in Java. Distributed and backed by persistent storage.

1. 생성 후 압축을 풀고 선호하는 IDE로 열기

2. JobRunr 의존성 추가

// build.gradle.kts
dependencies {
	// ..
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jobrunr:jobrunr-spring-boot-3-starter:7.1.0") // 추가

 

 과거 5.3.3까지는 합쳐져 있었는데 이후에는 boot 2와 3이 분리가 된 것으로 보인다.

  스프링 부트 3.2.5를 사용할 것이므로 jobrunr-spring-boot-3-starter:7.1.0를 사용하였습니다.

 

이 상태에서 애플리케이션을 수행하면 아래와 같은 에러가 발생합니다.

Parameter 0 of method jobScheduler in org.jobrunr.spring.autoconfigure.JobRunrAutoConfiguration required a bean of type 'org.jobrunr.storage.StorageProvider' that could not be found.

의미를 해석해보면 JobRunrAutoConfiguration 는 StorageProvider 타입의 빈이 필요하는데 제공이 되지 않았다는 의미입니다.

StorageProvider는 org.jobrunr.storage 패키지 아래에 있는 인터페이스입니다.

StorageProvider 인터페이스를 구현하고 있는 구체 타입은 여러가지가 있지만 주요 RDBMS 몇 가지만 클래스 다이어그램을 그려보면 아래와 같은 계층 구조를 가집니다.

 

jobrunr의 example-spring 프로젝트에 보면 JobServerConfiguration 설정으로 JdbcDataSource 빈을 설정을 해주고 StorageProvider 타입의 빈을 설정하는 부분은 따로 없습니다.

 

이유는 JobRunrSqlStorageAutoConfiguration에서 storageProvider라는 이름의 빈을 생성하는데 javax.sql.DataSource 타입의 빈을 기반으로 생성을 하기 때문입니다.

package org.jobrunr.spring.autoconfigure.storage;

@AutoConfiguration(after = DataSourceAutoConfiguration.class, before = JobRunrAutoConfiguration.class)
@ConditionalOnBean(DataSource.class)
@ConditionalOnProperty(prefix = "org.jobrunr.database", name = "type", havingValue = "sql", matchIfMissing = true)
public class JobRunrSqlStorageAutoConfiguration {

    @Bean(name = "storageProvider", destroyMethod = "close")
    @DependsOnDatabaseInitialization
    @ConditionalOnMissingBean
    public StorageProvider sqlStorageProvider(BeanFactory beanFactory, JobMapper jobMapper, JobRunrProperties properties) {
        String tablePrefix = properties.getDatabase().getTablePrefix();
        DatabaseOptions databaseOptions = properties.getDatabase().isSkipCreate() ? DatabaseOptions.SKIP_CREATE : DatabaseOptions.CREATE;
        StorageProvider storageProvider = SqlStorageProviderFactory.using(getDataSource(beanFactory, properties), tablePrefix, databaseOptions);
        storageProvider.setJobMapper(jobMapper);
        return storageProvider;
    }

    private DataSource getDataSource(BeanFactory beanFactory, JobRunrProperties properties) {
        if (isNotNullOrEmpty(properties.getDatabase().getDatasource())) {
            return beanFactory.getBean(properties.getDatabase().getDatasource(), DataSource.class);
        } else {
            return beanFactory.getBean(DataSource.class);
        }
    }
}

3. Spring Data JPA, H2 의존성 추가

// build.gradle.kts
dependencies {
	implementation("org.springframework.boot:spring-boot-starter-data-jpa") // 추가
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jobrunr:jobrunr-spring-boot-3-starter:7.1.0")
	runtimeOnly("com.h2database:h2") // 추가
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

의존성을 추가하면 이번에는 애플리케이션이 정상으로 뜨는 것을 볼 수 있습니다.

아랫 부분에 보면 Running migration MigrationByPath 으로 뜨는 로그가 있습니다.

2024-05-05T18:19:35.668+09:00  WARN 10329 --- [study-jobrunr] [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-05-05T18:19:35.875+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v000__create_migrations_table.sql}
2024-05-05T18:19:35.890+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v001__create_job_table.sql}
2024-05-05T18:19:35.892+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v002__create_recurring_job_table.sql}
2024-05-05T18:19:35.894+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v003__create_background_job_server_table.sql}
2024-05-05T18:19:35.895+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v004__create_job_stats_view.sql}
2024-05-05T18:19:35.901+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v005__update_job_stats_view.sql}
2024-05-05T18:19:35.903+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v006__alter_table_jobs_add_recurringjob.sql}
2024-05-05T18:19:35.913+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v007__alter_table_backgroundjobserver_add_delete_config.sql}
2024-05-05T18:19:35.918+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v008__alter_table_jobs_increase_jobAsJson_size.sql}
2024-05-05T18:19:35.919+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/h2/migrations/v009__change_jobrunr_job_counters_to_jobrunr_metadata.sql}
2024-05-05T18:19:35.923+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/h2/migrations/v010__change_job_stats.sql}
2024-05-05T18:19:35.924+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v011__change_sqlserver_text_to_varchar.sql}
2024-05-05T18:19:35.925+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v012__change_oracle_alter_jobrunr_metadata_column_size.sql}
2024-05-05T18:19:35.925+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v013__alter_table_recurring_job_add_createdAt.sql}
2024-05-05T18:19:35.928+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v014__improve_job_stats.sql}
2024-05-05T18:19:35.929+09:00  INFO 10329 --- [study-jobrunr] [           main] o.j.storage.sql.common.DatabaseCreator   : Running migration MigrationByPath{path=/org/jobrunr/storage/sql/common/migrations/v015__alter_table_backgroundjobserver_add_name.sql}
2024-05-05T18:19:36.013+09:00  INFO 10329 --- [study-jobrunr] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2024-05-05T18:19:36.017+09:00  INFO 10329 --- [study-jobrunr] [           main] c.e.s.StudyJobrunrApplicationKt          : Started StudyJobrunrApplicationKt in 1.839 seconds (process running for 2.165)

이 스크립트는 JobRunr이 실행할 때 필요한 테이블에 대한 생성을 수행합니다.

jobrunr-7.1.0.jar의 org.jobrunr.storage.sql.common.migrations 에 위치하고 있습니다.

 

# application.properties
spring.application.name=study-jobrunr
spring.jpa.open-in-view=false
spring.h2.console.path=/h2-console
spring.h2.console.enabled=true

H2 콘솔 화면으로 접속해서 보면 5개의 테이블과 1개의 뷰가 생성되어 있음을 알 수 있습니다.

- 접속시에 애플리케이션 로그에 "H2 console available at '/h2-console'" 부분을 찾으면 접속하는 JDBC URL을 확인 가능합니다.

4. JobRunr 대시보드 켜기

애플리케이션 설정에 아래 부분을 추가합니다.

# application.properties
spring.application.name=study-jobrunr
spring.jpa.open-in-view=false
spring.h2.console.path=/h2-console
spring.h2.console.enabled=true

org.jobrunr.dashboard.enabled=true
org.jobrunr.background-job-server.enabled=true
org.jobrunr.background-job-server.worker-count=2

다시 애플리케이션을 띄어보면 로그에 JobRunrDashboardWebServer 부분이 뜨고 JobRunr Dashboard 주소를 볼 수 있습니다.

2024-05-05T18:30:01.477+09:00  INFO 10452 --- [study-jobrunr] [           main] o.j.s.t.VirtualThreadJobRunrExecutor     : ThreadManager of type 'VirtualThreadPerTask' started
2024-05-05T18:30:01.490+09:00  INFO 10452 --- [study-jobrunr] [pool-2-thread-1] org.jobrunr.server.BackgroundJobServer   : JobRunr BackgroundJobServer (607e34af-aa0c-4754-ad82-211c067ac7f6) using H2StorageProvider and 2 BackgroundJobPerformers started successfully
2024-05-05T18:30:01.491+09:00  INFO 10452 --- [study-jobrunr] [           main] o.j.dashboard.JobRunrDashboardWebServer  : JobRunr Dashboard using H2StorageProvider started at http://localhost:8000/dashboard
2024-05-05T18:30:01.495+09:00  INFO 10452 --- [study-jobrunr] [pool-2-thread-1] org.jobrunr.server.ServerZooKeeper       : Server 607e34af-aa0c-4754-ad82-211c067ac7f6 is master (this BackgroundJobServer)
2024-05-05T18:30:01.497+09:00  INFO 10452 --- [study-jobrunr] [roundjob-worker] o.j.s.t.startup.MigrateFromV5toV6Task    : Start migration of scheduled jobs from v5 to v6
2024-05-05T18:30:01.500+09:00  INFO 10452 --- [study-jobrunr] [roundjob-worker] o.j.s.t.startup.MigrateFromV5toV6Task    : Found 0 scheduled jobs to migrate.
2024-05-05T18:30:01.500+09:00  INFO 10452 --- [study-jobrunr] [roundjob-worker] o.j.s.t.startup.MigrateFromV5toV6Task    : Finished migration of scheduled jobs from v5 to v6
2024-05-05T18:30:01.570+09:00  INFO 10452 --- [study-jobrunr] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2024-05-05T18:30:01.575+09:00  INFO 10452 --- [study-jobrunr] [           main] c.e.s.StudyJobrunrApplicationKt          : Started StudyJobrunrApplicationKt in 1.837 seconds (process running for 2.159)

5. 지연된 작업 수행 수행

JobRunr을 검토하게 된 이유에는 지연된 작업을 수행을 위한 목적이 있었다.

가령 문자 메시지 전송을 수행하고 전송 결과를 조회하는데 어떤 임의의 시간 간격이 필요했다.

일종의 fire-and-forgot 성 작업이 필요했다.

 

JobRunr의 Scheduling jobs이 딱 그 유스케이스에 적합했다.

 

SimpleService.kt

package com.example.studyjobrunr.core.service

import org.springframework.stereotype.Service

@Service
class SimpleService {
    fun doSimpleJob(anArgument: String) {
        println("Doing some work: $anArgument")
    }
}

 

JobEndpoint.kt

package com.example.studyjobrunr.webapp.api

import com.example.studyjobrunr.core.service.SimpleService
import org.jobrunr.scheduling.JobScheduler
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime.now

@RestController
@RequestMapping("/jobs")
class JobEndpoint(
    private val jobScheduler: JobScheduler,
) {

    @GetMapping(value = ["/simple-job"], produces = [MediaType.TEXT_PLAIN_VALUE])
    fun simpleJob(@RequestParam(defaultValue = "World") name: String): String {
        val enqueuedJobId = jobScheduler.schedule<SimpleService>(now().plusSeconds(5)) {
            it.doSimpleJob(anArgument = "Hello $name")
        }
        return "Job Enqueued: $enqueuedJobId"
    }

}

 

API를 통해 트리거를 걸면 job ID가 출력되면서 바로 응답은 끝난다.

http://localhost:8080/jobs/simple-job?name=seoul

그리고 10초 후에 콘솔에 "Doing some work: Hello seoul"라는 메시지가 찍힌다.

작업 목록에 가보면 아래와 같이 성공했음을 대시 보드에서 확인 가능하다.

같이 보기