스프링 프레임워크/springbatch

스프링배치 FlatFileItemReader 시작하기

blogger903 2024. 8. 7. 01:50
728x90

환경

IDE: intellij
SpringBootVersion: 3.0.11
Gradle: 8.8
Java: 17

 

해당 포스트는

한 번에 끝내는 Spring 완.전.판 초격차 패키지 Online. 의 강의를 코틀린으로 재구성한것입니다

 

다루는 내용

  • FlatFileItemReader
  • FieldSetMapper
  • JobParametersValidator

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.0.11"
    id("io.spring.dependency-management") version "1.1.0"
    kotlin("jvm") version "1.7.22"
    kotlin("plugin.spring") version "1.7.22"
    kotlin("plugin.jpa") version "1.7.22"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-batch")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.kafka:spring-kafka:3.2.0")
    implementation("mysql:mysql-connector-java:8.0.33")
    implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
    implementation("com.querydsl:querydsl-kotlin:5.0.0")

    annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.batch:spring-batch-test")
    testImplementation("org.junit.jupiter:junit-jupiter-api")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

Program arguments

--job.name=lawdInsertJob filePath=LAWD_CODE_SAMPLE.txt

lawdFileItemReader 설명

 

@Value("#{jobParameters['filePath']}"): Job 파라미터에서 'filePath' 값을 가져와 이 함수의 매개변수로 주입합니다. .delimited(): 파일의 각 라인이 구분자로 나누어진 필드들로 구성되어 있음을 나타냅니다.
.delimiter("\t"): 필드 구분자로 탭(\t)을 사용한다고 지정합니다.
.names(LAWD_CD, LAWD_DONG, EXIST_YN): 각 필드의 이름을 지정합니다. 이 이름들은 Lawd 객체의 프로퍼티 이름과 일치해야 합니다.
.linesToSkip(1): 파일의 첫 번째 줄(보통 헤더)을 건너뛰도록 설정합니다.
.encoding("EUC-KR"): 파일의 인코딩을 EUC-KR로 지정합니다.
.fieldSetMapper(LawdFieldSetMapper()): LawdFieldSetMapper라는 커스텀 FieldSetMapper를 사용하여 읽은 데이터를 Lawd 객체로 매핑합니다.
.resource(ClassPathResource(filePath)): 읽을 파일의 위치를 지정합니다. ClassPathResource는 클래스패스 내의 리소스를 나타냅니다.

 

 

Job

import com.example.kotlinsimplespringbatch.domain.Lawd
import com.example.kotlinsimplespringbatch.job.lawd.LawdFieldSetMapper.companion.EXIST_YN
import com.example.kotlinsimplespringbatch.job.lawd.LawdFieldSetMapper.companion.LAWD_CD
import com.example.kotlinsimplespringbatch.job.lawd.LawdFieldSetMapper.companion.LAWD_DONG
import com.example.kotlinsimplespringbatch.job.validator.FilePathParameterValidator
import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.core.configuration.annotation.JobScope
import org.springframework.batch.core.configuration.annotation.StepScope
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.item.ItemWriter
import org.springframework.batch.item.file.FlatFileItemReader
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import org.springframework.transaction.PlatformTransactionManager

@Configuration
class LawdInsertJobConfig(
    val jobRepository: JobRepository,
    val transactionManager: PlatformTransactionManager,
) {

    val chunkSize: Int = 1_000

    @Bean
    fun lawdInsertJob(lawdInsertStep: Step): Job {
        return JobBuilder("lawdInsertJob", jobRepository)
            .start(lawdInsertStep)
            .incrementer(RunIdIncrementer())
            .validator(FilePathParameterValidator())
            .build()
    }

    @JobScope
    @Bean
    fun lawdInsertStep(
        lawdFieldItemReader: FlatFileItemReader<Lawd>,
        lawdItemWriter: ItemWriter<Lawd>,
    ): Step {
        return StepBuilder("lawdInsertStep", jobRepository)
            .chunk<Lawd, Lawd>(chunkSize, transactionManager)
            .reader(lawdFieldItemReader)
            .writer(lawdItemWriter)
            .build()
    }

    @Bean
    @StepScope
    fun lawdFileItemReader(@Value("#{jobParameters['filePath']}") filePath: String): FlatFileItemReader<Lawd> {
        return FlatFileItemReaderBuilder<Lawd>()
            .name("lawdFileItemReader")
            .delimited()
            .delimiter("\t")
            .names(LAWD_CD, LAWD_DONG, EXIST_YN)
            .linesToSkip(1)
            .encoding("EUC-KR")
            .fieldSetMapper(LawdFieldSetMapper())
            .resource(ClassPathResource(filePath))
            .build()
    }

    @Bean
    @StepScope
    fun lawdItemWriter(): ItemWriter<Lawd> {
        return ItemWriter<Lawd> { items ->
            items.forEach(::println)
        }
    }
}

FieldSetMapper 구현

import com.example.kotlinsimplespringbatch.domain.Lawd
import com.example.kotlinsimplespringbatch.job.lawd.LawdFieldSetMapper.companion.EXIST_TRUE
import com.example.kotlinsimplespringbatch.job.lawd.LawdFieldSetMapper.companion.EXIST_YN
import com.example.kotlinsimplespringbatch.job.lawd.LawdFieldSetMapper.companion.LAWD_CD
import com.example.kotlinsimplespringbatch.job.lawd.LawdFieldSetMapper.companion.LAWD_DONG
import org.springframework.batch.item.file.mapping.FieldSetMapper
import org.springframework.batch.item.file.transform.FieldSet

class LawdFieldSetMapper: FieldSetMapper<Lawd> {

    object companion {
        const val LAWD_CD = "lawdCd"
        const val LAWD_DONG = "lawdDong"
        const val EXIST_YN = "existYn"
        const val EXIST_TRUE = "존재"
    }

    override fun mapFieldSet(fieldSet: FieldSet): Lawd {
        return Lawd(
            code = fieldSet.readString(LAWD_CD),
            dong = fieldSet.readString(LAWD_DONG),
            existYn = fieldSet.readBoolean(EXIST_YN, EXIST_TRUE)
        )
    }
}

JobParametersValidator

import org.springframework.batch.core.JobParameters
import org.springframework.batch.core.JobParametersInvalidException
import org.springframework.batch.core.JobParametersValidator
import org.springframework.core.io.ClassPathResource

class FilePathParameterValidator: JobParametersValidator {

    companion object {
        const val FILEPATH = "filePath"
    }

    override fun validate(parameters: JobParameters?) {
        val filePath = parameters?.getString(FILEPATH)
        if (filePath.isNullOrBlank()) {
            throw JobParametersInvalidException("$FILEPATH parameter is required")
        }

        val resource = ClassPathResource(filePath)
        if (!resource.exists()) {
            throw JobParametersInvalidException("$FILEPATH not found")
        }
    }
}

Resource file

법정동코드    법정동명    폐지여부
1100000000    서울특별시    존재
1111000000    서울특별시 종로구    존재
1111010100    서울특별시 종로구 청운동    존재
1111010200    서울특별시 종로구 신교동    존재
1111010300    서울특별시 종로구 궁정동    존재
1111010400    서울특별시 종로구 효자동    존재
1111010500    서울특별시 종로구 창성동    존재
1111010600    서울특별시 종로구 통의동    존재
1111010700    서울특별시 종로구 적선동    존재
1111010800    서울특별시 종로구 통인동    존재
1111010900    서울특별시 종로구 누상동    존재
1111011000    서울특별시 종로구 누하동    존재
1111011100    서울특별시 종로구 옥인동    존재
1111011200    서울특별시 종로구 체부동    존재
1111090100    서울특별시 종로구 창신1동    폐지
1111090200    서울특별시 종로구 창신2동    폐지
1111090300    서울특별시 종로구 창신3동    폐지
1111090400    서울특별시 종로구 숭인1동    폐지
1111090500    서울특별시 종로구 숭인2동    폐지