본문 바로가기

Programing/JVM(Java, Kotlin)

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

같이보기:

Kotlin 에 대한 내용은 [Kotlin] byte[]을 String으로 바꾸기 를 참고하세요.

주의:

실무에서 아래에 적혀있는 BigInteger를 이용해서 테스트를 진행하다가 특정한 상황일 때 제대로 변환을 못하는 경우를 발견하였다.

처음으로 시작하는 16진수 숫자가 7F 이하일 경우만 제대로 동작을 한다. 예를 들어 0xDD 일 경우는 앞에 0으로 채운다.

"dd20e7d3a2f1e814993511540a404615" 일 경우 크기가 16이 아닌 17개인 앞이 0으로 채워진 값을 반환하는 문제가 발생했다.

그래서 javax.xml.bind.DatatypeConverter의 parseHexBinary 를 쓰기를 권장한다. 2020-02-05
see https://inneka.com/programming/java/java-code-to-convert-byte-to-hexadecimal/

방법 1

StringBuilder 를 이용해서 한땀한땀 문자열을 만든다.

String byteArrayToHex(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (final byte b : bytes) {
        sb.append(String.format("%02X", b & 0xff));
    }
    return sb.toString();
}

방법2

BigInteger를 이용하여 변환한다.

byte[] bytes;  // ...
String.format("%02X", new BigInteger(1, bytes));

위의 경우는 "2479205A7E68AFA9" 와 같이 대문자로 만들 경우인데, 재미있는 것은 BigInteger.toString()의 경우 내부적으로 Long.toString(long i, int radix)를 사용하기 때문에 소문자로 만들어지는데, 대문자를 원하면 나온 문자열 결과를 toUpperCase()를 체이닝해서 쓰라고 주석으로 되어 있다.

따라서 위의 코드는 아래와 같다.

byte[] bytes;  // ...
new BigInteger(1, bytes).toString(16).toUpperCase();

소문자로 만든다면 아래처럼 toString(int radix)만 쓰면 된다. 

new BigInteger(1, bytes).toString(16);

사실 format이 호출되면 Object인 BigInteger는 내부적으로 toString() 이 호출된다.
이 내부를 보면 결국 StringBuilder를 이용해서 문자열을 만든다. 결국 내가 구현하나 라이브러리가 하나의 차이.

public String toString() {
    return toString(10);
}

public String toString(int radix) {
    if (signum == 0)
        return "0";
    if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
        radix = 10;

    // If it's small enough, use smallToString.
    if (mag.length <= SCHOENHAGE_BASE_CONVERSION_THRESHOLD)
       return smallToString(radix);

    // Otherwise use recursive toString, which requires positive arguments.
    // The results will be concatenated into this StringBuilder
    StringBuilder sb = new StringBuilder();
    if (signum < 0) {
        toString(this.negate(), sb, radix, 0);
        sb.insert(0, '-');
    }
    else
        toString(this, sb, radix, 0);

    return sb.toString();
}

반대의 변환은?

직접 구현한다면...

byte[] decodeWithHex(String hexStr) {
    int len = hexStr.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(hexStr.charAt(i), 16) << 4)
                + Character.digit(hexStr.charAt(i + 1), 16));
    }
    return data;
}

BigInteger를 쓴다면...

new BigInteger(hexStr, 16).toByteArray();

한땀 한 땀의 다른 버전...

private byte[] decodeHex(String cipherText) {
    char[] data = cipherText.toCharArray();
    int len = data.length;

    if ((len & 0x01) != 0) {
        throw new CryptographyException("Odd number of characters.");
    }

    byte[] out = new byte[len >> 1];

    // two characters form the hex value.
    for (int i = 0, j = 0; j < len; i++) {
        int f = toDigit(data[j], j) << 4;
        j++;
        f = f | toDigit(data[j], j);
        j++;
        out[i] = (byte) (f & 0xFF);
    }

    return out;
}

private int toDigit(char ch, int index) {
    int digit = Character.digit(ch, 16);
    if (digit == -1) {
        throw new CryptographyException("Illegal hexadecimal character " + ch + " at index " + index);
    }
    return digit;
}