https://integer-ji.tistory.com/440
Spring Boot 3.x에서 Chunk 사용해서 데이터 read, write 사용률 알아보기
0. Chunk를 사용하는 이유는 뭘까?메모리 관리에 용이전체 데이터를 한 번에 로드하지 않고 설정된 Chunk 크기만큼 메모리에 유지시킨다. 대용량 데이터 처리 시에도 OutOfMemoryError를 방지할 수 있다
integer-ji.tistory.com
0. Kotlin과 차이가 있을까?
저번주에는 Spring Boot에서 Java 17을 이용해 Chunk의 메모리 사용량을 알아보았다.
여기서 궁금한 점.
Kotlin에서 Java 소스를 그대로 사용하게 된다면 과연 메모리 사용율은 어떻게 변할까?
큰 차이가 있을지 궁금하여 시도해 보았다.
1. 프로젝트 설정
1-1. 사용된 언어와 라이브러리 버전
kotlin 1.9.25
spring boot 3.3.6
dependencies {
implementation("org.springframework.boot:spring-boot-starter-batch:3.3.6")
implementation("org.springframework.boot:spring-boot-starter-jdbc:3.3.6")
implementation("org.jetbrains.kotlin:kotlin-reflect")
compileOnly("org.projectlombok:lombok:1.18.36")
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:12.6.4.jre11")
annotationProcessor("org.projectlombok:lombok:1.18.36")
testImplementation("org.springframework.boot:spring-boot-starter-test:3.3.6")
testImplementation("org.springframework.batch:spring-batch-test:5.1.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.5")
}
1-2. 테이블 생성 및 데이터 Insert (mssql)
2. 배치 생성
Chunk를 사용하여 데이터를 Read, Write 하는 함수를 만듭니다.
package com.kin.batch.config
import com.kin.batch.listener.PerformanceStepListener
import com.kin.batch.model.InputType
import com.kin.batch.model.OutputType
import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.core.job.builder.JobBuilder
import org.springframework.batch.core.repository.JobRepository
import org.springframework.batch.core.step.builder.StepBuilder
import org.springframework.batch.item.ItemProcessor
import org.springframework.batch.item.database.JdbcBatchItemWriter
import org.springframework.batch.item.database.JdbcCursorItemReader
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder
import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.PlatformTransactionManager
import javax.sql.DataSource
@Configuration
class BatchConfig(private val dataSource: DataSource) {
@Bean
fun batchJob(
jobRepository: JobRepository,
transactionManager: PlatformTransactionManager
): Job {
return JobBuilder("batchJob", jobRepository)
.start(step1(jobRepository, transactionManager))
.build()
}
@Bean
fun step1(
jobRepository: JobRepository,
transactionManager: PlatformTransactionManager
): Step {
return StepBuilder("step1", jobRepository)
.chunk<InputType, OutputType>(1000, transactionManager)
.reader(itemReader())
.processor(itemProcessor())
.writer(itemWriter())
.listener(PerformanceStepListener())
.allowStartIfComplete(false)
.build()
}
@Bean
fun itemReader(): JdbcCursorItemReader<InputType> {
return JdbcCursorItemReaderBuilder<InputType>()
.name("jdbcItemReader")
.dataSource(dataSource)
.sql("SELECT id, name, value, created_at FROM test_table")
.rowMapper { rs, _ ->
InputType(
rs.getString("name"),
rs.getInt("value"),
rs.getTimestamp("created_at").toLocalDateTime()
)
}
.build()
}
@Bean
fun itemProcessor(): ItemProcessor<InputType, OutputType> {
return ItemProcessor { input ->
OutputType(
name = input.name.uppercase(),
value = input.value * 2,
createdAt = input.createdAt
)
}
}
@Bean
fun itemWriter(): JdbcBatchItemWriter<OutputType> {
return JdbcBatchItemWriterBuilder<OutputType>()
.dataSource(dataSource)
.sql("INSERT INTO processed_table (name, value, created_at) VALUES (:name, :value, :createdAt)")
.beanMapped()
.build()
}
}
나머지는 이전 글과 동일하다.
3. 메모리 체크 함수
package com.kin.batch.listener
import org.springframework.batch.core.ExitStatus
import org.springframework.batch.core.StepExecution
import org.springframework.batch.core.listener.StepExecutionListenerSupport
import java.lang.management.ManagementFactory
import com.sun.management.OperatingSystemMXBean
import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
class PerformanceStepListener : StepExecutionListenerSupport() {
private val runtime: Runtime = Runtime.getRuntime()
override fun afterStep(stepExecution: StepExecution): ExitStatus {
// 시작 시간과 종료 시간 가져오기
val startTime: LocalDateTime = stepExecution.startTime ?: LocalDateTime.now()
val endTime: LocalDateTime = stepExecution.endTime ?: LocalDateTime.now()
// Instant 변환
val startInstant: Instant = localDateTimeToInstant(startTime)
val endInstant: Instant = localDateTimeToInstant(endTime)
// 실행 시간 계산 (밀리초)
val executionTimeMs: Long = Duration.between(startInstant, endInstant).toMillis()
val cpuUsage: Float = getCpuUsage()
val memoryUsage: Double = getMemoryUsage()
printPerformanceMetrics(stepExecution, executionTimeMs, cpuUsage, memoryUsage)
return stepExecution.exitStatus
}
private fun localDateTimeToInstant(localDateTime: LocalDateTime): Instant {
return localDateTime.atZone(ZoneId.systemDefault()).toInstant()
}
private fun getCpuUsage(): Float {
return try {
val osBean: OperatingSystemMXBean =
ManagementFactory.getPlatformMXBean(OperatingSystemMXBean::class.java)
// 첫 번째 샘플링
val startTime: Long = System.nanoTime()
val startCpuTime: Double = osBean.processCpuTime.toDouble()
// 100ms 대기
Thread.sleep(100)
// 두 번째 샘플링
val endTime: Long = System.nanoTime()
val endCpuTime: Double = osBean.processCpuTime.toDouble()
// CPU 사용률 계산
val elapsedTime: Long = endTime - startTime // 나노초
val cpuUsage: Double = (endCpuTime - startCpuTime) / (elapsedTime * osBean.availableProcessors)
// 퍼센트 단위로 변환
(cpuUsage * 100).toFloat()
} catch (e: Exception) {
e.printStackTrace()
0.0f // 오류 시 기본값
}
}
private fun getMemoryUsage(): Double {
// JVM 메모리 사용량 측정 (MB)
val usedMemory: Long = runtime.totalMemory() - runtime.freeMemory()
return usedMemory / (1024.0 * 1024.0) // MB 단위로 변환
}
private fun printPerformanceMetrics(
stepExecution: StepExecution,
executionTimeMs: Long,
cpuUsage: Float,
memoryUsage: Double
) {
System.out.printf(
"""
=== Batch Step Performance Metrics ===
Step Name: %s
Execution Time: %d ms
CPU Usage: %.2f %%
Memory Usage: %.2f MB
Items Read: %d
Items Written: %d
======================================
""".trimIndent(),
stepExecution.stepName,
executionTimeMs,
cpuUsage,
memoryUsage,
stepExecution.readCount,
stepExecution.writeCount
)
}
}
4. 배치 수행
테스트는 Java와 마찬가지로 1,500,000건으로 테스트를 진행해 보았다.
mssql을 사용했고 서버는 docker container로 올렸다.
이번에는 배치 수행 전 평시 상태를 체크해 보았다.
평온하다.
평온한 도커에 돌을 던져보자
4-1. 애플리케이션 수행
=== Batch Step Performance Metrics ===
Step Name: step1
Execution Time: 74689 ms
CPU Usage: 0.09 %
Memory Usage: 143.88 MB
Items Read: 1500000
Items Written: 1500000
======================================
앗 Java 보다 자원을 많이 쓴 거 같은데?
도커 DB CPU는 74% 정도 사용했다.
(java는 80% 넘게 사용됐다.)
청크를 높여보자
chunk 5000
=== Batch Step Performance Metrics ===
Step Name: step1
Execution Time: 52788 ms
CPU Usage: 0.01 %
Memory Usage: 117.95 MB
Items Read: 1500000
Items Written: 1500000
======================================
마찬가지로 메모리 사용량이 되게 높다.
CPU는 77.68%
애플리케이션의 메모리를 정리해 본다면
chunk 크기 | 실행 시간(ms) | CPU 사용률 (%) | 메모리 사용량 (MB) |
1000 | 74689 | 0.09 | 143.88 |
5000 | 52788 | 0.01 | 117.95 |
흠 정리를 해보자.
5. 마무리
5-1. 비교 정리
실행 시간
* Java (chunk 1000): 80,352ms
* Kotlin (chunk 1000): 74,689ms
Kotlin이 약 7% 더 빠른 실행 속도를 보임
애플리케이션 메모리 사용
* Java (chunk 1000): 48.31MB
* Kotlin (chunk 1000): 143.88MB
Kotlin이 약 3배 더 많은 메모리를 사용
DB CPU 사용률
Java: 최대 80% 이상
Kotlin: 최대 74%
Kotlin이 DB CPU를 조금 더 효율적으로 사용
5-2. 정리
DB 서버의 리소스 사용과 전반적인 실행 속도에서는 kotiln이 더 우세한 성능을 보였다. 하지만 어플리케이션 메모리 사용량에서는 Java가 더 효율적이었다. 이 이유는 kotiln이 런타임 라이브러리가 크기때문에 많이 사용되는 거라고 추정된다.
그리고 kotiln을 사용하는데 정말 편했다. 나는 Python과 Java를 사용하고 있는데 딱 두개의 장점을 섞어놓은 듯한 느낌을 받았다. 특히 java에서 만든 소스 그대로 kotiln에서 사용하는게 너무 편했다
5-3. 짧은 회고
kotiln을 연구하는 이유는 많은 회사에서 kotiln을 도입한 이유를 알고 싶었고 이미 고착화된 java를 버리고 kotiln으로 넘어가야하나?라는 의문점에 답변을 하고싶었다.
그렇게 Java와 Kotiln을 비교해가며 사용해보니 각각의 장점을 알 수 있었다. 또한 이러한 연구방식은 처음 시도해 보는데 CPU, 메모리의 사용량을 알기위해 라이브러리도 처음 써보고 Docker 컨테이너를 사용해서 상태분석도 해봤다. 물론 이게 옳지 않을 수도 있다. 하지만 이러한 방향성을 잡은 지금으로써는 더 다듬어가며 앞으로의 연구에 도움이 될 것 같아서 많이 뿌듯하다.
'코딩공부' 카테고리의 다른 글
Apache Kafka를 실행해보자 (0) | 2025.02.16 |
---|---|
Kotlin 런타임 시간 개선하기 (1) | 2025.02.02 |
Spring Boot 3.x에서 Chunk 사용해서 데이터 read, write 사용률 알아보기 (4) | 2024.12.24 |
Java와 Kotlin, 컴파일 속도 차이가 많이날까? (1) | 2024.10.29 |
Kotlin을 알아보자 (0) | 2024.10.27 |