Series/실전!

RandomCode Generator 성능 측정하기

Hyunec 2023. 1. 11. 16:40
일련번호는 총 16 자리의 숫자/영문 대문자로 이루어진 문자열이다.

최근 회사 업무를 하던 중 기존에 작성된 무작위 일련번호를 만드는 로직을 보게 되었습니다.
간단한 업무 내용에 Util 성격의 로직이었지만, 구현된 로직은 생각보다 복잡했습니다.
그래서 기존 로직의 의도를 파악하고, 개선을 시도해 보았습니다.

먼저 결론을 공유하고, 시도한 개선을 따라가 보겠습니다.

결론 (각 1만, 10만, 50만, 100만 회 반복)

  • ThreadLocalRandom 의 성능은 매우 뛰어나다. Kotlin Random 역시 뛰어나다.
  • String 변환은 매우 비싼 작업이며 Sequence 가 효율적으로 작동하지 않을 수 있다.
  • Coroutine 은 학습이 부족하여 적용하지 않았다.
 

GitHub - Hyune-s-lab/perf-16-digit-code-generator: 16자리 코드 생성 성능 테스트

16자리 코드 생성 성능 테스트. Contribute to Hyune-s-lab/perf-16-digit-code-generator development by creating an account on GitHub.

github.com

 

기존 로직 분석

CodeGenerator.kt
object CodeGenerator {
    private val numUpperChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()
    private const val codeDigits = 16

    fun generate(count: Long): Set<String> {
        val codes = mutableSetOf<String>()
        val code = StringBuilder()
        do {
            repeat((1..codeDigits).count()) {
                getRandomIndex().apply {
                    code.append(numUpperChars[this])
                }
            }
            codes.add(code.toString())
            code.clear()
        } while (!isGeneratedCountEnough(codes, count))

        return codes
    }

    private fun getRandomIndex() = ThreadLocalRandom.current().nextInt(0, numUpperChars.size)
    private fun isGeneratedCountEnough(codes: Set<String>, count: Long) = codes.size >= count
}
  • 숫자와 영문 대문자만을 가진 Character Pool 을 활용하여 낭비 없이 랜덤 문자를 가져옵니다.
  • StringBuilder 를 통해 코드 생성 과정의 낭비를 없애고, Set 자료구조를 통해 완성된 코드의 중복을 방지합니다.
  • ThreadLocalRandom 의 활용으로 안전하고 빠른 Random 을 구현했습니다.
 

Random 대신 ThreadLocalRandom을 써야 하는 이유

java.util.Random은 멀티 쓰레드 환경에서 하나의 인스턴스에서 전역적으로 의사 난수(pseudo random)를 반환한다. 따라서 같은 시간에 동시 요청이 들어올 경우 경합 상태에서 성능에 문제가 생길 수 있

velog.io

 

숫자와 문자로만 이루어진 16자리라면 UUID 를 활용하면 어떨까?

UUIDStrategy.kt
UUIDWithSequenceStrategy.kt

  • 검증된 UUID 로직을 활용하기에 성능이 좋아질 것으로 기대했지만, 기존 대비 2배 수준의 성능 차이가 납니다. 
class UUIDStrategy : CodeGenerationStrategy {
    override fun generate(size: Long): Set<String> =
        (1..size)
            .map { UUID.randomUUID().toString().uppercase().replace("-", "") }
            .toSet()
}
  • 가독성은 확실히 좋아졌지만, String 변환의 비용이 크기 때문인 것 같습니다.

 

ThreadLacalRandom, Java Random, Kotlin Random 은 어떨까?

LegacyWithJavaRandomStrategy.kt
LegacyWithKotlinRandomStrategy.kt

  • Java Random 은 꽤 느립니다.
  • Kotlin Random 과 ThreadLocalRandom 은 큰 차이가 없습니다.
class LegacyWithJavaRandomStrategy : CodeGenerationStrategy {
    override fun generate(size: Long): Set<String> {
	...
    }

	// companion object 에 선언한 random 사용
    private fun getRandomIndex() = random.nextInt(0, numUpperChars.size) 
    private fun isGeneratedCountEnough(codes: Set<String>, count: Long) = codes.size >= count
    
    companion object {
        private val numUpperChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()
        private val random = Random() // Java Random 선언
        private const val codeDigits = 16
    }
}


class LegacyWithKotlinRandomStrategy : CodeGenerationStrategy {
    override fun generate(size: Long): Set<String> {
	...
	}

	// Kotlin Random 사용
    private fun getRandomIndex() = Random.nextInt(0, numUpperChars.size)
    private fun isGeneratedCountEnough(codes: Set<String>, count: Long) = codes.size >= count
    
    companion object {
        private val numUpperChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()
        private const val codeDigits = 16
    }
}
  • Java Random 은 사용 전 객체 생성을 해줘야 합니다. 본 코드에서는 companion object 에 선언하였습니다.
  • Kotlin Random 은 바로 사용할 수 있었습니다. (이미 companion object)

 

UUID 를 그대로 쓰면 어떨까?

PerformanceTest.kt

fun measureTimeMillis3(size: Long): Long {
    var codes: List<UUID>
    val elapsed = measureTimeMillis {
        codes = (1..size).asSequence()
            .map { UUID.randomUUID() }
            .toList()
    }

    codes.size shouldBe size

//    println(codes)
    return elapsed
}
  • UUID 는 최적화가 잘되어서인지 아주 높은 성능을 보여주었습니다.
  • Sequence 도 잘 적용됩니다.

 

커스텀 로직보다는 검증된 라이브러리를 활용하는 것의 성능이 더 좋을 것이다.

직접 해보기 전까지는 위와 같이 생각했었습니다.
하지만 직접 해보면서 업무 성격에 따라서는 직접 만드는 것이 더 유리할 수도 있다는 것을 알았고, 기존 코드를 학습하면서 ThreadLocalRandom 이라는 몰랐던 기능과 Java Random 과 Kotlin Random 의 차이도 알 수 있었습니다.

비록 가독성은 차이가 있지만, 기존 로직이 업무 요건을 만족하고 성능 차이가 크기에 그대로 둘 것 같습니다.