1. 개요

Java에서 파일 크기를 얻을일반적으로 값을 바이트 단위로 얻습니다. 그러나 일단 파일이 충분히 커지면(예: 123456789바이트) 바이트로 표현된 길이를 보는 것은 파일의 크기를 이해하려는 우리에게 어려운 일이 됩니다.

이 사용방법(예제)에서는 바이트 단위의 파일 크기를 Java에서 사람이 읽을 수 있는 형식으로 변환하는 방법을 살펴봅니다.

2. 문제 소개

앞에서 이야기한 것처럼 바이트 단위의 파일 크기가 크면 사람이 이해하기 쉽지 않습니다. 따라서 우리는 많은 양의 데이터를 인간에게 제시할 때 KB, MB, GB 등과 같은 적절한 SI 접두사를 사용하여 많은 수를 인간이 읽을 수 있도록 합니다. 예를 들어 "270GB"는 "282341192 바이트"보다 훨씬 이해하기 쉽습니다.

그러나 표준 Java API를 통해 파일 크기를 얻을 때 일반적으로 바이트 단위입니다. 따라서 사람이 읽을 수 있는 형식을 가지려면 값을 바이트 단위에서 해당 바이너리 접두사로 동적으로 변환해야 합니다. 예를 들어 "282341192바이트"를 "207MiB"로 변환하거나 "2048바이트"를 "2KiB"로 변환합니다. .

단위 접두사에는 두 가지 변형이 있음을 언급할 가치가 있습니다.

  • 이진 접두사 – 1024의 거듭제곱입니다. 예를 들어 1MiB = 1024KiB, 1GiB = 1024MiB 등입니다.
  • SI( International System of Units ) 접두어 - 1000의 거듭제곱입니다. 예를 들어 1MB = 1000KB, 1GB = 1000MB 등입니다.

우리 예제은 이진 접두사와 SI 접두사 모두에 초점을 맞출 것입니다.

3. 문제 해결

우리는 이미 문제 해결의 열쇠가 적합한 단위를 동적으로 찾는 것임을 깨달았을 것입니다.

예를 들어 입력이 1024보다 작은 경우, 예를 들어 200이면 바이트 단위를 "200 바이트"로 가져와야 합니다. 그러나 입력이 1024보다 크고 1024 * 1024보다 작은 경우(예: 4096) KiB 단위를 사용해야 하므로 "4 KiB"가 됩니다.

그러나 문제를 단계별로 해결해 봅시다. 단위 결정 로직을 살펴보기 전에 먼저 필요한 모든 단위와 그 경계를 정의해 보겠습니다.

3.1. 필요한 단위 정의

우리가 알고 있듯이 1단위에 1024를 곱하면 다음 단계의 단위로 전환됩니다 . 따라서 기본 값으로 필요한 모든 단위를 나타내는 상수를 만들 수 있습니다.

private static long BYTE = 1L;
private static long KiB = BYTE << 10;
private static long MiB = KiB << 10;
private static long GiB = MiB << 10;
private static long TiB = GiB << 10;
private static long PiB = TiB << 10;
private static long EiB = PiB << 10;

위의 코드에서 볼 수 있듯이 기본 값을 계산하기 위해 이진 왼쪽 시프트 연산자 (<<)를 사용했습니다. 여기서 x << 10 ”은 1024가 2의 10승이기 때문에 “ x * 1024 ” 와 동일합니다 .

SI 접두사의 경우 하나의 단위에 1000을 곱하면 다음 수준의 단위로 이동합니다 . 따라서 기본 값으로 필요한 모든 단위를 나타내는 상수를 만들 수 있습니다.

private static long KB = BYTE * 1000;
private static long MB = KB * 1000;
private static long GB = MB * 1000;
private static long TB = GB * 1000;
private static long PB = TB * 1000;
private static long EB = PB * 1000;

3.1. 숫자 형식 정의

올바른 단위를 결정했고 파일 크기를 소수점 이하 두 자리로 표현하려는 경우 결과를 출력하는 메서드를 만들 수 있습니다.

private static DecimalFormat DEC_FORMAT = new DecimalFormat("#.##");

private static String formatSize(long size, long divider, String unitName) {
    return DEC_FORMAT.format((double) size / divider) + " " + unitName;
}

다음으로 메서드가 수행하는 작업을 빠르게 이해해 보겠습니다. 위의 코드에서 본 것처럼 먼저 숫자 형식 DEC_FORMAT을 정의했습니다.

구분자 매개변수는 선택한 단위의 기본 값이고 문자열 인수 unitName 단위 이름입니다. 예를 들어 KiB를 적합한 단위로 선택한 경우, Divider=1024unitName = “KiB”입니다.

이 방법은 나눗셈 계산과 숫자 형식 변환을 중앙 집중화합니다.

이제 솔루션의 핵심 부분인 올바른 단위 찾기로 이동할 때입니다.

3.2. 단위 결정

먼저 단위 결정 방법의 구현을 살펴보겠습니다.

public static String toHumanReadableBinaryPrefixes(long size) {
    if (size < 0)
        throw new IllegalArgumentException("Invalid file size: " + size);
    if (size >= EiB) return formatSize(size, EiB, "EiB");
    if (size >= PiB) return formatSize(size, PiB, "PiB");
    if (size >= TiB) return formatSize(size, TiB, "TiB");
    if (size >= GiB) return formatSize(size, GiB, "GiB");
    if (size >= MiB) return formatSize(size, MiB, "MiB");
    if (size >= KiB) return formatSize(size, KiB, "KiB");
    return formatSize(size, BYTE, "Bytes");
}
public static String toHumanReadableSIPrefixes(long size) {
    if (size < 0)
        throw new IllegalArgumentException("Invalid file size: " + size);
    if (size >= EB) return formatSize(size, EB, "EB");
    if (size >= PB) return formatSize(size, PB, "PB");
    if (size >= TB) return formatSize(size, TB, "TB");
    if (size >= GB) return formatSize(size, GB, "GB");
    if (size >= MB) return formatSize(size, MB, "MB");
    if (size >= KB) return formatSize(size, KB, "KB");
    return formatSize(size, BYTE, "Bytes");
}

이제 방법을 살펴보고 작동 방식을 이해해 보겠습니다.

먼저 입력이 양수인지 확인해야 합니다.

그런 다음 하이(EB)에서 로우(Byte) 방향으로 단위를 확인합니다. 입력 크기 가 현재 단위의 기본 값보다 크거나 같으면 현재 단위가 올바른 단위가 됩니다.

올바른 단위를 찾는 즉시 이전에 만든 formatSize 메서드를 호출하여 최종 결과를 String 으로 얻을 수 있습니다 .

3.3. 솔루션 테스트

이제 솔루션이 예상대로 작동하는지 확인하는 단위 테스트 방법을 작성해 보겠습니다. 메서드 테스트를 단순화하기 위해 입력 및 해당 예상 결과를 보유 하는 Map <Long, String> 을 초기화해 보겠습니다.

private static Map<Long, String> DATA_MAP_BINARY_PREFIXES = new HashMap<Long, String>() {{
    put(0L, "0 Bytes");
    put(1023L, "1023 Bytes");
    put(1024L, "1 KiB");
    put(12_345L, "12.06 KiB");
    put(10_123_456L, "9.65 MiB");
    put(10_123_456_798L, "9.43 GiB");
    put(1_777_777_777_777_777_777L, "1.54 EiB");
}};
private final static Map<Long, String> DATA_MAP_SI_PREFIXES = new HashMap<Long, String>() {{
    put(0L, "0 Bytes");
    put(999L, "999 Bytes");
    put(1000L, "1 KB");
    put(12_345L, "12.35 KB");
    put(10_123_456L, "10.12 MB");
    put(10_123_456_798L, "10.12 GB");
    put(1_777_777_777_777_777_777L, "1.78 EB");
}};

다음으로 Map DATA_MAP 을 살펴보고 각 키 값을 입력으로 사용하고 예상 결과를 얻을 수 있는지 확인합니다.

DATA_MAP.forEach((in, expected) -> Assert.assertEquals(expected, FileSizeFormatUtil.toHumanReadable(in)));

단위 테스트를 실행하면 통과합니다.

4. 열거형 및 루프로 솔루션 개선

지금까지 문제를 해결했습니다. 해결책은 매우 간단합니다. toHumanReadable 메서드 에서 단위를 결정하기 위해 여러 if 문을 작성했습니다.

솔루션에 대해 신중하게 생각하면 몇 가지 오류가 발생할 수 있습니다.

  • 이러한 if 문의 순서는 메서드에 있는 그대로 고정되어야 합니다.
  • if 문에서 단위 상수와 해당 이름을 String 개체로 하드 코딩했습니다.

다음으로 솔루션을 개선하는 방법을 살펴보겠습니다.

4.1. SizeUnit 열거형 만들기

사실, 메서드에서 이름을 하드 코딩할 필요가 없도록 단위 상수를 열거형 으로 변환할 수 있습니다.

enum SizeUnitBinaryPrefixes {
    Bytes(1L),
    KiB(Bytes.unitBase << 10),
    MiB(KiB.unitBase << 10),
    GiB(MiB.unitBase << 10),
    TiB(GiB.unitBase << 10),
    PiB(TiB.unitBase << 10),
    EiB(PiB.unitBase << 10);

    private final Long unitBase;

    public static List<SizeUnitBinaryPrefixes> unitsInDescending() {
        List<SizeUnitBinaryPrefixes> list = Arrays.asList(values());
        Collections.reverse(list);
        return list;
    }
   //getter and constructor are omitted
}
enum SizeUnitSIPrefixes {
    Bytes(1L),
    KB(Bytes.unitBase * 1000),
    MB(KB.unitBase * 1000),
    GB(MB.unitBase * 1000),
    TB(GB.unitBase * 1000),
    PB(TB.unitBase * 1000),
    EB(PB.unitBase * 1000);

    private final Long unitBase;

    public static List<SizeUnitSIPrefixes> unitsInDescending() {
        List<SizeUnitSIPrefixes> list = Arrays.asList(values());
        Collections.reverse(list);
        return list;
     }
    //getter and constructor are omitted
}

위의 열거형 SizeUnit 에서 볼 수 있듯이 SizeUnit 인스턴스는 unitBasename 을 모두 보유합니다 .

또한 나중에 "내림차순" 순서로 단위를 확인하고 싶기 때문에 필요한 순서로 모든 단위를 반환 하는 도우미 메서드 unitsInDescending을 만들었습니다.

열거형 을 사용 하면 이름을 수동으로 코딩할 필요가 없습니다.

다음으로 일련의 if을 개선할 수 있는지 살펴보겠습니다 .

4.2. 루프를 사용하여 단위 결정

우리의 SizeUnit enum 은 List 의 모든 단위를 내림차순으로 제공할 수 있으므로 if 문 집합을 for 루프로 바꿀 수 있습니다.

public static String toHumanReadableWithEnum(long size) {
    List<SizeUnit> units = SizeUnit.unitsInDescending();
    if (size < 0) {
        throw new IllegalArgumentException("Invalid file size: " + size);
    }
    String result = null;
    for (SizeUnit unit : units) {
        if (size >= unit.getUnitBase()) {
            result = formatSize(size, unit.getUnitBase(), unit.name());
            break;
        }
    }
    return result == null ? formatSize(size, SizeUnit.Bytes.getUnitBase(), SizeUnit.Bytes.name()) : result;
}

위의 코드에서 볼 수 있듯이 메서드는 첫 번째 솔루션과 동일한 논리를 따릅니다. 또한 이러한 단위 상수, 여러 if 문 및 하드 코딩된 단위 이름을 피합니다.

예상대로 작동하는지 확인하기 위해 솔루션을 테스트해 보겠습니다.

DATA_MAP.forEach((in, expected) -> Assert.assertEquals(expected, FileSizeFormatUtil.toHumanReadableWithEnum(in)));

테스트를 실행하면 테스트가 통과됩니다.

5. Long.numberOfLeadingZeros 메서드 사용

단위를 하나씩 확인하고 조건을 만족하는 첫 번째 단위를 선택하여 문제를 해결했습니다.

또는 Java 표준 API의 Long.numberOfLeadingZeros 메서드를 사용하여 주어진 크기 값이 속하는 단위를 결정할 수 있습니다.

다음으로 이 흥미로운 접근 방식을 자세히 살펴보겠습니다.

5.1. Long.numberOfLeadingZeros 메서드 소개

Long.numberOfLeadingZeros 메서드 는 지정된 Long의 이진 표현에서 가장 왼쪽 1비트 앞에 있는 0비트 수를 반환합니다 .

Java의 Long 유형은 64비트 정수이므로 Long.numberOfLeadingZeros (0L) = 64 입니다. 몇 가지 예는 방법을 빠르게 이해하는 데 도움이 될 수 있습니다.

1L  = 00... (63 zeros in total) ..            0001 -> Long.numberOfLeadingZeros(1L) = 63
1024L = 00... (53 zeros in total) .. 0100 0000 0000 -> Long.numberOfLeadingZeros(1024L) = 53

이제 Long.numberOfLeadingZeros 메서드를 이해했습니다. 그러나 단위를 결정하는 데 도움이 되는 이유는 무엇입니까?

알아 봅시다.

5.2. 문제를 해결하기 위한 아이디어

우리는 단위 사이의 인수가 1024라는 것을 알고 있습니다. 이는 2의 10제곱( 2^10 )입니다. 따라서 각 단위의 기본 값에 선행하는 0의 수를 계산하면 인접한 두 단위 간의 차이는 항상 10입니다 .

Index  Unit	numberOfLeadingZeros(unit.baseValue)
----------------------------------------------------
0      Byte	63
1      KiB  	53
2      MiB  	43
3      GiB  	33
4      TiB  	23
5      PiB  	13
6      EiB       3

또한 입력 값의 선행 0의 수를 계산하고 결과가 적절한 단위를 찾기 위해 단위 범위에 속하는지 확인할 수 있습니다.

다음으로, 단위를 결정하고 크기 4096에 대한 단위 기본 값을 계산하는 방법에 대한 예를 살펴보겠습니다.

if 4096 < 1024 (Byte's base value)  -> Byte 
else:
    numberOfLeadingZeros(4096) = 51
    unitIdx = (numberOfLeadingZeros(1) - 51) / 10 = (63 - 51) / 10 = 1
    unitIdx = 1  -> KB (Found the unit)
    unitBase = 1 << (unitIdx * 10) = 1 << 10 = 1024

다음으로 이 논리를 메서드로 구현해 보겠습니다.

5.3. 아이디어 구현

방금 논의한 아이디어를 구현하는 방법을 만들어 봅시다.

public static String toHumanReadableByNumOfLeadingZeros(long size) {
    if (size < 0) {
        throw new IllegalArgumentException("Invalid file size: " + size);
    }
    if (size < 1024) return size + " Bytes";
    int unitIdx = (63 - Long.numberOfLeadingZeros(size)) / 10;
    return formatSize(size, 1L << (unitIdx * 10), " KMGTPE".charAt(unitIdx) + "iB");
}

보시다시피 위의 방법은 매우 간단합니다. 단위 상수나 enum 이 필요하지 않습니다 . 대신 단위를 포함하는 문자열 을 만들었습니다 : ” KMGTPE” . 그런 다음 계산 된 unitIdx 를 사용하여 올바른 단위 문자를 선택하고 "iB"를 추가하여 완전한 단위 이름을 만듭니다.

문자열 ” KMGTPE” 에서 일부러 첫 번째 문자를 비워둔다는 점을 언급할 가치가 있습니다. 이는 " Byte " 단위가 " *B " 패턴을 따르지 않고 별도로 처리했기 때문입니다. if (size < 1024) return size + " Bytes";

다시 한 번 테스트 메서드를 작성하여 예상대로 작동하는지 확인합니다.

DATA_MAP.forEach((in, expected) -> Assert.assertEquals(expected, FileSizeFormatUtil.toHumanReadableByNumOfLeadingZeros(in)));

6. Apache Commons IO 사용

지금까지 파일 크기 값을 사람이 읽을 수 있는 형식으로 변환하는 두 가지 접근 방식을 구현했습니다.

실제로 일부 외부 라이브러리는 이미 문제를 해결하기 위한 방법 인 Apache Commons-IO 를 제공 했습니다.

Apache Commons-IO의 FileUtils 를 사용하면 byteCountToDisplaySize 메서드를 통해 바이트 크기를 사람이 읽을 수 있는 형식으로 변환할 수 있습니다.

단, 이 방법은 소수점 이하를 자동으로 반올림합니다 .

마지막으로 입력 데이터로 byteCountToDisplaySize 메서드를 테스트하고 출력되는 내용을 확인합니다.

DATA_MAP.forEach((in, expected) -> System.out.println(in + " bytes -> " + FileUtils.byteCountToDisplaySize(in)));

테스트 결과:

0 bytes -> 0 bytes
1024 bytes -> 1 KB
1777777777777777777 bytes -> 1 EB
12345 bytes -> 12 KB
10123456 bytes -> 9 MB
10123456798 bytes -> 9 GB
1023 bytes -> 1023 bytes

7. 결론

이 문서에서는 파일 크기(바이트)를 사람이 읽을 수 있는 형식으로 변환하는 다양한 방법을 다루었습니다.

항상 그렇듯이 이 기사에 제시된 코드는 GitHub 에서 사용할 수 있습니다 .

Generic footer banner