본문 바로가기

Programing/JVM(Java, Kotlin)

[Kotlin] byte[]을 String으로 바꾸기

과거에 [Java] byte[]을 String으로 바꾸기 글을 쓴 적이 있다.

 

요즘은 코틀린을 주로 사용을 해서 글을 다시 써보았다.

방법에 대해서는 https://www.baeldung.com/kotlin/byte-arrays-to-hex-strings 에 잘 나와 있기에 링크를 참고하는 것이 더 좋다.

 

최초 코드

원래 코드을 method extract 로 리팩토링을 해보니 아래와 같이 표현을 할 수 있는 코드였다. (원래코드는 아래에 나옵니다. ^^;;)

private fun bytesToHexString(bytes: ByteArray): String {
    val hash = StringBuilder()
    for (aByte in bytes) {
    	val hex = Integer.toHexString(0xFF and aByte.toInt())
        if (hex.length == 1) {
            hash.append('0')
        }
    }
    return hash.toString()
}

 

최초 코드 리뷰 때 장호님이 왜 길이가 1이면 0을 붙이는지에 대해 문의를 주셨다.

생각해보니 byte[] 를 16진수 문자열로 바꾸는 코드라 할 수 있다.

일단 의문을 남기게 하는 코드는 냄새가 난다고 볼 수 있다.

사실 원래 코드는...

위의 코드는 기능을 분리를 해서 이해하기 쉬워졌다. 하지만 원래 아래처럼 해싱하는 부분과 같이 붙어있었다.
따라서 인지 부하를 높혀서 이해를 떨어뜨렸을 것이라 생각한다.

override fun hash(payload: String): String {
    val sha256HMAC = Mac.getInstance(ALGORITHM)
    sha256HMAC.init(secretKeySpec)

    val hash = StringBuilder()

    val bytes = sha256HMAC.doFinal(payload.toByteArray())
    for (aByte in bytes) {
        val hex = Integer.toHexString(0xFF and aByte.toInt())
        if (hex.length == 1) {
            hash.append('0')
        }
        hash.append(hex)
    }

    return hash.toString()
}

리팩토링 후 해싱을 하는 부분은 아래와 같이 단순해졌다.

override fun hash(payload: String): String {
    val sha256HMAC = Mac.getInstance(ALGORITHM)
    sha256HMAC.init(secretKeySpec)
    val payloadInBytes = payload.toByteArray()
    val bytes: ByteArray = sha256HMAC.doFinal(payloadInBytes)

    return bytesToHexString(bytes)
}

장호 님의 추천 코드

두 번째 리뷰 때 장호님은 "%02X" 를 사용을 문의하셨다.

물론 계획은 있습니다. ㅎㅎㅎ

X 와 x 는 한글자 차이지만 값 자체가 달라지니 주의해야 한다.

다행히 테스트 코드가 잘 잡아주어서 "%02X" 가 아닌 "%02x" 로 적용을 했다. 16진수 값은 FF 나 ff 로 나타낼 수 있지만 문자열 상으로는 다른 값이다.

빈약한 테스트라서 아쉽지만 이런 것이 변화에 대한 안전망 역할을 해주기에 소중하다.

private fun bytesToHexString(bytes: ByteArray): String {
    val hash = StringBuilder()
    for (aByte in bytes) {
        val hex = "%02x".format(aByte)
        hash.append(hex)
    }
    return hash.toString()
}

이제 녹색이 된다. 그러고보니 깨지는 테스트는 이제 적색이 아닌 오렌지색이다.

혹시나 해서 코틀린의 확장 함수로 적용을 해보았다.

fun ByteArray.toHex(): String = joinToString(separator = "") {
    eachByte -> "%02x".format(eachByte)
}

그런데 생각보다 성능이 떨어지는 것을 느껴졌다. 단위테스트의 시간이 세 자리로 바뀌었기 때문이었다.
15.4ms 정도 느려져서 사실 큰 차이는 아닐 수 있지만 레이턴시에 민감한 서비스라면 아쉬운 시간일 수 있기에 %02x 를 사용하기로 했다.

 

데이터가 일단 빈약하긴 하지만...

성능 차이에 대해 깊게 생각은 못했지만 아마도 joinToString 를 통해 합칠 때 객체를 계속 생성을 하게 되는 것이 성능을 떨어뜨리는 요소가 될 것으로 추측했다. StringBuilder 의 경우 내부적으로는 길이에 대해 재할당을 받기는 하지만 mutable 한 문자열이라 재할당에 대한 부담이 아무래도 적을 것이기 때문이다.

스트림 처리나 이벤트 프로세싱, 함수형 처리 등에서 불변에 대해 장점을 내새우지만 어떨 때는 가변 처리가 나은 경우가 있는데 이것이 그런 케이스라고 할 수 있겠다. "은총알은 없다"라는 말을 다시 새삼 느꼈다.