지나공 : 지식을 나누는 공간
String -> 음수 hashCode -> Hex -> Long 변환 시 NumberFormatException 트러블슈팅 본문
우당탕탕 삽질기
String -> 음수 hashCode -> Hex -> Long 변환 시 NumberFormatException 트러블슈팅
해리리_ 2026. 2. 22. 19:21
타 팀에서 Kafka 메시지를 발행하면 NiFi를 거쳐 우리팀 Kafka Consumer가 해당 메시지를 수신하는 프로세스가 있다.
이 과정에는 아래 단계가 포함되는데, 가끔 길이가 긴 문자열로 된 Kafka Key가 들어왔을 때 3번 단계에서 NumberFormatException 이 발생했었다.
- String인 Kafka Key 를 hashCode 로 전환
- hashCode를 Hex Encoding 함.
- Hex Encoding 된 값을 fromRadix(16) 메소드를 활용해 Long 타입 숫자로 변환함. (parseLong)
원인을 정확하게 확인하지 못하고 넘어갔었는데 최근에 간단한 내용이었음을 알게 되었다.
사전 지식
길이가 긴 문자열을 hashCode(int) 전환 시 음수가 되는 케이스
문자열을 hashCode (int) 로 전환하면 아래와 같은 식을 거치다보니, 문자열 길이가 길어지면 int 범위를 초과하여 오버플로가 발생하면서 음수 hashCode로 전환될 수 있다.
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

Hex Encoding된 값을 parseLong 시도 시 양수/음수 판단이 제대로 이뤄지지 않는 케이스
parseLong은 signedLong으로 해석하기 때문에 들어온 값이 양수인지 음수인지 판단해야하는데, 이때 "-" 라는 문자가 포함되었는지를 보고 음수인지를 판단한다. 즉
FFFFFFFFBA44B6E1와 같은 음수인 16진수 값을 제대로 음수로 취급하지 못할 수 있다. 따라서 음수로 취급해야할 값을 양수로 취급하면서 값이 매우 커지고 signed long 범위를 벗어날 수 있다. paseUnsignedLong 메소드에서는 범위 내에 속하여 정상적으로 변환됐다.

정상 케이스) 문자열 길이가 짧고 양수 hashCode로 전환될 때
- "02715" 라는 적당한 길이의 문자열
- hashCode (int) 로 변환 시 결과는 45872985
- 위 값을 Hex Encoding 해서 16진수로 만든 결과는 0000000002BBF759
- 16진수 값을 parseLong 하여 숫자로 변환 시 45872985 로 정상 변환되어 2번의 값을 동일하게 얻는다.
실패 케이스) 문자열 길이가 짧고 양수 hashCode로 전환될 때
- "0659506596" 라는 조금 더 긴 길이의 문자열
- hashCode (int) 로 변환 시 결과는 -1169901855. 오버플로 발생하면서 음수로 나옴.
- 위 값을 Hex Encoding 해서 16진수로 만든 결과는 FFFFFFFFBA44B6E1
- 16진수 값을 parseLong 하여 숫자로 변환 시 위 사전지식에서 말한 것처럼 signed long으로 해석을 시도하는데, "-" 라는 문자열이 없으니 양수로 해석했고, 이때 18446744072554600161 와 같이 큰 값으로 변환되면서 sigend long 범위를 벗어나서 NumberFormatException 이 발생한다.
- parseUnsignedLong 으로 하면 정상적으로 다시 -1169901855 를 얻을 수 있다.
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class Test {
/**
* 정상 케이스: 문자열이 양수 hashCode 로 전환되는 경우
*/
@Test
fun positiveHashFlow() {
val str = "02715"
// 1. String -> hashCode()
val hashCode = str.hashCode()
println("1. hashcode 결과 (int): $hashCode")
assertEquals(45872985, hashCode)
// 2. hashCode -> Hex Encoding
val hexString = java.lang.Long.toHexString(hashCode.toLong())
println("2. hex 변환 (String): $hexString")
assertEquals("2bbf759", hexString)
val padded = "%016X".format(hashCode.toLong())
println("2. padded hex: $padded")
assertEquals("0000000002BBF759", padded)
// 3. Hex → Long
val parsedLong = java.lang.Long.parseLong(hexString, 16)
println("3. toLong(16) 변환 (long): $parsedLong")
assertEquals(45872985, parsedLong)
val parsedUnsignedLong = java.lang.Long.parseUnsignedLong(hexString, 16)
println("3. unsignedLong: $parsedUnsignedLong")
assertEquals(45872985, parsedUnsignedLong)
}
/**
* 실패 케이스: 문자열이 음수 hashCode 로 전환되는 경우
*/
@Test
fun negativeHashFlow() {
val str = "0659506596"
// 1. String -> hashCode()
val hashCode = str.hashCode()
println("1. hashcode 결과 (int): $hashCode")
assertEquals(-1169901855, hashCode)
// 2. hashCode -> Hex Encoding
val hashCodeLong = hashCode.toLong()
val hexString = java.lang.Long.toHexString(hashCodeLong)
println("2. hex 변환 (String): $hexString")
assertEquals("ffffffffba44b6e1", hexString)
val padded = "%016X".format(hashCode.toLong())
println("2. padded hex: $padded")
assertEquals("FFFFFFFFBA44B6E1", padded)
// 3. Hex → Long
assertThrows(NumberFormatException::class.java) {
java.lang.Long.parseLong(hexString, 16)
}
val parsedUnsignedLong = java.lang.Long.parseUnsignedLong(hexString, 16)
println("3. unsignedLong: $parsedUnsignedLong")
assertEquals(-1169901855, parsedUnsignedLong)
}
}728x90
'우당탕탕 삽질기' 카테고리의 다른 글
Comments