[Java] byte to hexString

식빵·2021년 12월 19일
0

Java Lab

목록 보기
2/29
post-thumbnail

회사에서 로그인 처리와 관련된 코드를 잠시 보고 있었는데 아래와 같은 코드가 있었다.


String pw = "user01";

MessageDigest digest = MessageDigest.getInstance("SHA-256");

byte[] encodedHash = digest.digest(pw.getBytes(StandardCharsets.UTF_8));

// encodedHash 는 32 바이트(고정적인 바이트 수를 뽑아낸다) ==> 헥사로는 64자리 문자열이 나온다. 

StringBuilder builder = new StringBuilder(digest.length * 2);

for (int i = 0; i < digest.length; i++) {
    String hex 
        = Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1).toLowerCase();
    builder.append(hex);
}

... 생략 ...

위의 코드는 회원가입 데이터를 받아오면 해당 비밀 번호를 Sha256 암호화를 거치고,
생성된 byte[] 로 Hex String 을 생성하는 것이다.

여기서 궁금한 건 바로 이 코드다
Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1).toLowerCase();

이게 HexString을 만드는 코드인데... 처음 봤을 때는 도통 이해가 안됐다.

차근 차근 왜 그런지 아래 순서대로 알아보자.

  • digest[i] & 0xff
  • + 0x100
  • .substring(1)



1. digest[i] & 0xff ??

digest[i] 는 byte 타입이기 때문에 일단 1byte, 즉 8bit의 정보만 담을 수 있다.
그런데 다른 언어와는 다르게 Java에는 unsigned 라는 개념을 안 쓰다보니,
byte 로 표현 가능한 숫자는 -128~127 만 가능하다.

간단 테스트

public static void main(String[] args) {

    // OVERFLOW 이전
    int integerNumber = 127;
    byte byteNumber = (byte) integerNumber;

    System.out.println("OVERFLOW 이전");
    System.out.println("integerNumber: " + integerNumber);
    System.out.println("byteNumber: " + byteNumber);

    // OVERFLOW 이후
    integerNumber = 128;
    byteNumber = (byte) integerNumber;

    System.out.println("\nOVERFLOW 이전");
    System.out.println("integerNumber: " + integerNumber);
    System.out.println("byteNumber: " + byteNumber);
}

출력결과:

OVERFLOW 이전
integerNumber: 127
byteNumber: 127

OVERFLOW 이전
integerNumber: 128
byteNumber: -128

그래서 우리가 8bit로 0~255 까지의 값을 나타내고 싶다면
어쩔 수 없이 1byte가 아닌 4byte의 공간을 갖는 integer 로 형변환이 필요하다.
이를 위해서 0xff(integer 형)에 & 연산을 통하여 암묵적 형변환이 일어나게 한다.

참고로 리터럴로 작성한 정수형 숫자는 기본적으로 integer 형을 갖는다.



2. + 0x100 ??

앞서 1번을 하면 일단 overflow가 일어나는 것을 방지할 수 있다.
그런데 overflow 가 방지했다고 치자. 그다음에 0~255의 값을 받을 수 있는데,
문제는 우리가 8bit 중에서 앞의 4bit는 안 쓰고, 뒤의 4bit만 쓴 상태로 HexString을
만들면 어떻게 될까?

십진수binaryhex
2551111 11110xff
150000 11110x0f

아마 이쁘게 0x0f 딱 2자리로 떨어지길 바라겠지만, 어림없다.
Integer.toString(15, 16) 만 달랑 하면 0xff 가 아니라 0xf 가 나온다.

이를 방지하기 위한 게 +0x100 이다.
앞서 말한 십진수 15에 0x100을 더하면 아래와 같이 된다.

  0000 0000 1111
 +0001 0000 0000
 ----------------
  0001 0000 1111  ===> hex String: 0x10f 우리가 원했던 앞의 4bit를 위한 `0`이 붙었다.
  하지만 그 과정에서 맨앞에 `1`이 생겼다.

이렇듯 byte 가 앞의 4bit를 쓰든 안 쓰든 항상 0 이 생성되도록 강제하는 것이다.
하지만 이 과정에서 3자리의 hex String이 생성되고 무조건 맨앞에 1이 붙는다.



3. .substring(1) ??

그렇다면 +0x100 을 통해서 필요없이 붙은 맨 앞의 hex String 인 1 을 제거할 필요가 있다.
그렇기 때문에 Integer.toString((digest[i] & 0xff) + 0x100, 16)) 연산 이후에
.subString(1) 을 해주는 것이다.

테스트 코드

public static void main(String[] args) throws NoSuchAlgorithmException {
    
	byte b = (byte) 15;        
    // hex String은 잘 나오지만 1자리면 앞에 0 이 없다.
    System.out.println(Integer.toHexString(b & 0xff));	
    
    // 방법1
    // + 0x100 함으로써 앞에 2자리의 hex string 중 첫번째 자리에 문자열이 있는 것을 보장한다.
    System.out.println(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
    
    // 방법2: toHexString의 복잡함을 단순화. String.format 사용하기
    // 하지만 format은 속도가 느리다고 한다. 자주 호출되는 것이 아니면 괜찮을 듯하다.
    System.out.println(String.format("%02x", b));
	
}

별거 아닌데 참 복잡하다.



마치며

사실 이외에도 byte 를 hexString으로 변경하는 방법은 다양하다.
지금 이 글을 읽고 더 간단한 방법을 원한다면 좀 더 구글링해보자 😁

참고: https://www.baeldung.com/java-byte-arrays-hex-strings



보충: Java 17 의 HexFormat

2024-11-15 에 작성된 추가 내용입니다.

Java 17 부터는 더 간단하게 이를 처리할 수 있게 되었습니다.

import java.util.HexFormat;

public static void main(String[] args) {
	String result = HexFormat.of().formatHex(new byte[]{(byte)255, (byte)255});
    System.out.println("result = " + result);
    // 출력: result = ffff
}
profile
백엔드를 계속 배우고 있는 개발자입니다 😊

1개의 댓글

comment-user-thumbnail
2023년 12월 4일

궁금한 부분이 정확하게 여기에 있네요! 잘 보고 가요!

답글 달기