이번 글에서는 JPA를 적용한 Spring Batch Job을 테스트하는 글을 작성하겠습니다.
모든 소스는 Github에 올려두었습니다.
들어가기 전에
테스트 환경에서의 데이터베이스는 H2, Test Docker Container, Local 환경에 Database 띄우기 등 다양한 방법으로 테스트할 수 있습니다. 하지만 이번 글에서는 실제 DB에 붙었을 경우 테스트 데이터를 생성 삭제하기 위해서는 어떻게 하는지에 대해 작성했습니다.
Job
먼저 JOB을 살펴보겠습니다.
@Configuration
class SimpleJobConfiguration(
val jobBuilderFactory: JobBuilderFactory,
val stepBuilderFactory: StepBuilderFactory,
val entityManagerFactory: EntityManagerFactory,
val personRepository: PersonRepository,
val simpleStepListener: SimpleStepListener,
val simpleProcessor: SimpleProcessor,
val simpleWriter: SimpleWriter
) {
@Bean("${BatchItem.SIMPLE}_JOB")
fun simpleJob() =
jobBuilderFactory.get("${BatchItem.SIMPLE}_JOB")
.start(simpleStep(null))
.build()
@Bean("${BatchItem.SIMPLE}_STEP")
@JobScope
fun simpleStep(@Value("#{jobParameters[chunkSize]}") chunkSize: Int?) =
stepBuilderFactory.get("${BatchItem.SIMPLE}_STEP")
.chunk<Person, Person>(chunkSize!!)
.reader(reader(null))
.processor(simpleProcessor)
.writer(simpleWriter)
.listener(simpleStepListener)
.build()
@Bean("${BatchItem.SIMPLE}_READER")
@StepScope
fun reader(@Value("#{jobParameters[pageSize]}") pageSize: Int?): QuerydslPagingItemReader<Person> {
val reader = QuerydslPagingItemReader(entityManagerFactory) { personRepository.findAllInBatch() }
reader.pageSize = pageSize!!
return reader
}
}
이 코드에 대해서는 몇 가지 포인트만 집고 넘어가겠습니다.
- JobScope, StepScope를 사용하여 JobParameters를 받음
- QuerydslPagingItemReader로 Querydsl 기반인 Reader를 사용
- Reader, Processor, Writer 방식 사용
Test Code
Test Code 환경 구성
먼저 Spring Batch에 필요한 Annotation에 대해서 살펴보겠습니다.
@SpringBootTest( // 1)
classes = [
SimpleJobConfiguration::class, // 2)
SimpleStepListener::class, // 2)
SimpleProcessor::class, // 2)
SimpleWriter::class, // 2)
P6spyLogMessageFormatConfiguration::class, // 2)
TestBatchConfig::class // 3)
]
)
internal class SimpleJobConfigurationTest: BatchTestSupport() { // 4)
}
1) 먼저 @SpringBootTest로 Test에 필요한 의존성을 주입하였습니다.
( SpringBatch 공식문서에는 @ContextConfiguration 가이드되어 있으나 관련 Bean 주입을 하나하나 등록해야하는 불편함이 있어 @SpringBooTest를 선택하였습니다. )
2) Test Target이 되는 Job, Step, Listener를 선언합니다.
( P6spyLogMessageFormatConfiguration 클래스는 Query를 이쁘게 찍어주는 클래스입니다. 관련 설정은 여기를 참조해주세요 )
3) Batch Test에 필요한 Annotation을 모아놓은 클래스입니다.
@Configuration
@EnableAutoConfiguration // 3-1)
@EnableBatchProcessing // 3-2)
@EntityScan("me.practice.kotlinbatch.common.domain.entity") // 3-3)
@EnableJpaRepositories("me.practice.kotlinbatch.common.repository") // 3-3)
@Import(QuerydslConfiguration::class) // 3-4)
class TestBatchConfig {
}
3-1) spring.factories 에 등록된 설정들을 조건에 따라 Bean에 등록하는 Annotation
3-2) 배치 환경을 구성하는 Annotation
3-3) JPA 기반 Batch 이므로 Entity, Repository의 경로를 선언합니다.
3-4) Querydsl-JPA Configuration 클래스 파일을 등록합니다.
@Configuration
class QuerydslConfiguration(@PersistenceContext val entityManager: EntityManager) {
@Bean
fun jpaQueryFactory() : JPAQueryFactory = JPAQueryFactory(entityManager)
}
4) Batch Test를 편리하게 도와주는 클래스입니다.
@SpringBatchTest
open class BatchTestSupport {
@Autowired
protected lateinit var jobLauncherTestUtils: JobLauncherTestUtils
@Autowired
protected lateinit var entityManagerFactory: EntityManagerFactory
protected val entityManager: EntityManager by lazy { entityManagerFactory.createEntityManager() }
/**
* 유일한 값을 포함하는 Job Parameter Builder를 얻어온다.
* @return JobParametersBuilder
*/
protected fun getUniqueParameterBuilder() = jobLauncherTestUtils.uniqueJobParametersBuilder
/**
* Job 실행
* @return JobExecution
*/
protected fun launchJob() = jobLauncherTestUtils.launchJob()
/**
* Job 실행 ( Parameter 포함 )
* @param jobParameters JobParameters
* @return JobExecution
*/
protected fun launchJob(jobParameters: JobParameters) = jobLauncherTestUtils.launchJob(jobParameters)
/**
* Entity Save
* @param entity Any
* @return Any
*/
protected fun save(entity: Any): Any {
entityManager.persist(entity)
return entity
}
/**
* Entity Save All
* @param entities List<Any>
* @return List<Any>
*/
protected fun saveAll(entities: List<Any>): List<Any> {
entities.forEach { entity ->
entityManager.persist(entity)
}
return entities
}
/**
* Entity Delete
* @param entity Any
* @return Any
*/
protected fun delete(entity: Any): Any {
entityManager.remove(entity)
return entity
}
/**
* Entity Delete All
* @param entities List<Any>
* @return List<Any>
*/
protected fun deleteAll(entities: List<Any>): List<Any> {
entities.forEach { entity ->
entityManager.remove(entity)
}
return entities
}
}
여기서 포인트는 JpaRepository를 사용하지 않고 EntityManager에서 직접 persist, remove를 사용한다는 점 입니다.
Spring-Batch는 자체적으로 Transactional을 관리하고 있습니다. 그런데 테스트 코드에서 @Transactional을 선언하고 사용하면 아래와 같은 에러가 발생합니다.
Existing transaction detected in JobRepository. Please fix this and try again (e.g. remove @Transactional annotations from client).
java.lang.IllegalStateException: Existing transaction detected in JobRepository. Please fix this and try again (e.g. remove @Transactional annotations from client).
해석하자면 JobRepository에서 이미 Transaction이 존재합니다. @Transactional을 제거하십시오.
결론적으로 @Transactional을 사용하지 않고 Job 시작과 후에 Transaction을 시작하고 commit 하기 위해 EntityManager를 이용하였습니다.
테스트 코드 데이터 생성
배치 Job에 필요한 테스트 데이터를 생성하고 삭제처리하는 코드를 살펴보겠습니다.
lateinit var people: List<Person>
@BeforeEach
fun before() {
people = (0..20).map {
Person(name = "사람$it", address = "주소$it")
}
val transaction = entityManager.transaction
transaction.begin()
saveAll(people) // 영속성 맺음
transaction.commit()
}
@AfterEach
fun after() {
val transaction = entityManager.transaction
transaction.begin()
deleteAll(people) // 영속성이 맺어져 있어 삭제 가능
transaction.commit()
entityManager.clear() // 영속성 Finish
}
BatchTestSupport 클래스의 매서드를 이용하여 데이터를 저장하고 삭제합니다.
개인적으로 deleteAll() 메서드 사용을 권장하지 않습니다. 이유는 테스트환경을 잘 만들어서 실 DB에 문제가 없도록 설계했겠지만 개발자의 실수로 실 DB에 붙어 모든 데이터를 삭제할 수 있수도 있기 때문입니다.
결론은 EntityManager의 remove() 메서드를 사용하여 지정된 Entity의 데이터만 삭제하도록 처리하였습니다. remove() 메서드를 사용하려면 영속성이 맺어져 있어야하므로 @AfterEach 블록에서 EntityManager를 clear 하였습니다.
테스트 코드 작성
Batch Job을 테스트하는 코드를 살펴보겠습니다.
@Test
fun `test job run test`() {
// Given
val jobParameters = getUniqueParameterBuilder() // 아래 코드 참조
.addLong("chunkSize", 5L)
.addLong("pageSize", 20L)
.toJobParameters()
// When
val jobExecution = launchJob(jobParameters) // 아래 코드 참조
// Then
Assertions.assertThat(jobExecution.status).isEqualTo(BatchStatus.COMPLETED)
}
Job에서 Parameters를 받도록 설정했기 때문에 Parameter를 설정하고 Job을 Launch합니다.
/**
* 유일한 값을 포함하는 Job Parameter Builder를 얻어온다.
* @return JobParametersBuilder
*/
protected fun getUniqueParameterBuilder() = jobLauncherTestUtils.uniqueJobParametersBuilder
/**
* Job 실행 ( Parameter 포함 )
* @param jobParameters JobParameters
* @return JobExecution
*/
protected fun launchJob(jobParameters: JobParameters) = jobLauncherTestUtils.launchJob(jobParameters)
전체 테스트 코드
전체 코드를 살펴보겠습니다.
@SpringBootTest(
classes = [
SimpleJobConfiguration::class,
SimpleStepListener::class,
SimpleProcessor::class,
SimpleWriter::class,
P6spyLogMessageFormatConfiguration::class,
TestBatchConfig::class
]
)
internal class SimpleJobConfigurationTest: BatchTestSupport() {
lateinit var people: List<Person>
@BeforeEach
fun before() {
people = (0..20).map {
Person(name = "사람$it", address = "주소$it")
}
val transaction = entityManager.transaction
transaction.begin()
saveAll(people)
transaction.commit()
}
@AfterEach
fun after() {
val transaction = entityManager.transaction
transaction.begin()
deleteAll(people)
transaction.commit()
entityManager.clear()
}
@Test
fun `test job run test`() {
// Given
val jobParameters = getUniqueParameterBuilder()
.addLong("chunkSize", 5L)
.addLong("pageSize", 20L)
.toJobParameters()
// When
val jobExecution = launchJob(jobParameters)
// Then
Assertions.assertThat(jobExecution.status).isEqualTo(BatchStatus.COMPLETED)
}
}
'Develop > spring-batch' 카테고리의 다른 글
[Kotlin] Spring-Batch QuerydslPagingItemReader 개선편 (1) | 2021.08.04 |
---|---|
[kotlin] Spring-Batch Alert 처리 ( Logback을 이용한 Slack 연동 ) (0) | 2021.08.02 |
[Kotlin] Spring-Batch 적용 (0) | 2021.03.25 |
Spring batch에 Spring Data JPA 기반 Querydsl을 적용해보자! (QuerydlsPagingItemReader) (0) | 2020.11.09 |
Spring batch 스케줄 생성! [Jenkins] (0) | 2020.11.04 |