Kotlin 압축 성능 개선, jmh 벤치마크 활용

hbjs97·2025년 2월 4일
post-thumbnail

Kotlin 압축 성능 개선과 JMH 벤치마크 활용기

기존 서비스에서 사용하는 Kotlin 기반 압축 방식의 성능이 기대보다 매우 낮아, 다양한 방법으로 성능 개선을 시도했다. 하지만 널리 사용되는 압축 포맷을 유지하면서 만족할 만한 성능을 얻기 어려웠다.

본 포스팅에서는 Kotlin 기반 압축 방식의 문제점을 정확하게 파악하기 위해 JMH(Java Microbenchmark Harness)를 활용하여 신뢰할 수 있는 벤치마크를 수행했고, Go 언어와 OS 기반 도구(pigz)를 이용한 성능 개선 방안을 검토한 내용을 공유한다.

1. 기존 압축 방식의 문제점

기존 Kotlin 표준 라이브러리를 사용하여 압축할 경우 압축 속도가 매우 느려, 서비스 운영 환경에서 성능 병목이 발생하는 문제가 있었다. 특히 대용량 파일 압축 작업에서 현저히 성능이 떨어졌다.

이를 해결하기 위해 다음의 방안을 검토했다.

  • OS 수준의 빠른 압축 도구 활용 (pigz)
  • Go 언어 기반 라이브러리 활용 (pgzip)

2. 정확한 성능 측정을 위한 JMH 설정 방법

# build.gradle.kts

plugins {
    id("me.champeau.jmh") version "0.7.2"
    ...
}

dependencies {
    ...
    jmh("org.openjdk.jmh:jmh-core:1.37")
    jmh("org.openjdk.jmh:jmh-generator-annprocess:1.37")
}

jmh {
    zip64.set(true)
}

...
import io.kotest.common.runBlocking
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.BenchmarkMode
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Level
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Mode
import org.openjdk.jmh.annotations.OutputTimeUnit
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.TearDown
import org.openjdk.jmh.annotations.Warmup
import org.openjdk.jmh.infra.Blackhole
import java.io.File
import java.util.UUID
import java.util.concurrent.TimeUnit


@Fork(1)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)  // JVM 워밍업
@Measurement(iterations = 50, time = 1, timeUnit = TimeUnit.SECONDS)  // 실제 측정 반복 횟수
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
class Compress {
    private val projectTitle: String = "perfTest"
    private lateinit var baseDir: String
    private val sourceDir: File = File("/Users/hbjs/Documents/dummy")

    private val progressCallback: (ProgressUpdate) -> Unit = {}

    private val originalCompressor = FlowDataCompressor()
    private val pigzCompressor = FlowDataPigzCompressor()

    @Setup(Level.Iteration)
    fun setupIteration() {
        baseDir = "tmp/${UUID.randomUUID()}"
    }

    @Benchmark
    fun original(bh: Blackhole) {
        runBlocking {
            bh.consume(
                originalCompressor.compress(
                    projectTitle = projectTitle,
                    baseDir = baseDir,
                    sourceDir = sourceDir,
                    progressCallback = progressCallback
                )
            )
        }
    }

    @Benchmark
    fun pigz(bh: Blackhole) {
        runBlocking {
            bh.consume(
                pigzCompressor.compress(
                    projectTitle = projectTitle,
                    baseDir = baseDir,
                    sourceDir = sourceDir,
                    progressCallback = progressCallback
                )
            )
        }
    }

    @TearDown(Level.Iteration)
    fun tearDownIteration() {
        File(baseDir).deleteRecursively()
    }

    @TearDown
    fun teardown() {
        pigzCompressor.close()
    }
}

3. 벤치마크 결과 비교

약 290MB 크기의 파일을 기준으로 각 방식의 성능을 측정하여 비교했다.

  • 테스트 환경: M1 MacBook Pro, 32GB RAM
  • 측정 도구: Kotlin(JMH), Go(Shell 스크립트)
LanguageBenchmarkModeCntScoreErrorUnitsSpeed ImprovementPerformance Improvement비고 (Remarks)
Kotlin(original)jmhavgt506061.011± 25.658ms/op--Kotlin 표준 라이브러리 사용
Kotlin + OS(pigz)jmhavgt502266.745± 12.233ms/op약 2.7배약 63%OS 레벨에서 pigz 호출
Go(pgzip)sh--600-800-ms/op약 7.6배약 87%OS 캐시로 인해 Cold Cache 측정 어려움

Kotlin에서 OS의 외부 도구(pigz)를 활용하는 방식으로 기존 대비 약 63%의 성능 개선이 이루어졌다. Go를 이용한 방식은 Kotlin 대비 월등히 빠르나, 언어의 변경 및 환경의 복잡성을 증가시킬 수 있다는 점에서 당장 적용하기는 어려울 것으로 예상되므로 추후 고려.

go get github.com/klauspost/pgzip
go run main.go <SOURCE_DIR> <OUTPUT>.tar.gz

추후 정말 큰 성능 개선이 필요하다면, 압축 처리를 별도의 서버로 분리하고, 미디어 서버와 통신하면서 공유된 볼륨을 통해 압축 작업을 진행한 뒤 반환하는 방식이 유효할 것으로 예상된다. Kubernetes 환경에서는 미디어 pod에 사이드카(sidecar) 형태의 압축 container를 추가하는 방식을 고려할 수 있다.

주의사항 및 고려 사항

  • 위 방식의 경우 공유 볼륨에 담기는 파일 크기가 매우 커질 경우 문제가 발생할 수 있다.
  • 볼륨 공유 방식도 결국 File I/O 기반이므로, 추가적인 성능 이슈가 생길 수 있다.
  • 이러한 문제를 피하기 위해 별도의 압축 서버를 완전히 분리한 뒤 S3 등 별도의 스토리지 서버를 통해 압축한 파일을 업로드하고, 업로드된 파일의 URL을 반환하는 방식도 가능하다. 하지만 이 방법 역시 업로드가 병목이 될 가능성이 있다.
  • 미디어 서버와 압축 서버 간 TCP 커넥션을 열어 callback 방식으로 처리할 수 있지만, 시스템의 복잡성이 증가할 수 있다는 점을 주의해야 한다.
  • 위 방식들 모두 적절하지 않은 경우, Spring Boot 환경에서 OS에 설치된 빠른 압축 도구(예: pigz)를 활용하는 방식도 고려할 수 있다. 테스트 결과 Kotlin 기본 압축 방식보다는 매우 빨랐지만, OS 쓰레드를 직접 활용하므로 서버의 하드웨어 성능이 받쳐줘야 한다.

결론

Kotlin 기반 압축 성능 문제를 확인하고 JMH를 활용하여 신뢰성 높은 성능 측정을 수행했다. 결과적으로 Kotlin 기본 압축 방식 대비 OS 기반 압축 도구(pigz) 활용 시 약 63%의 성능 향상을 달성했다. Go 언어를 활용한 방식도 큰 성능 향상을 보였으나, 언어 변경과 시스템 복잡성으로 인해 현재 단계에서는 참고 목적으로만 활용할 계획이다.

앞으로 운영 환경에서의 추가 테스트를 통해 실사용 시 성능을 모니터링하고, 필요 시 Kubernetes 환경에서의 아키텍처 개선과 별도 압축 서버 구축 등 더욱 최적화된 압축 방식을 지속적으로 검토하여 서비스의 안정성과 성능을 더욱 향상시킬 예정이다.

0개의 댓글