for 문에서는 절대 float을 증감변수로 사용하지 않는다

등록일: 2014. 08. 08

자바 코딩, 이럴 땐 이렇게: PMD로 배우는 올바른 자바 코딩 방법

  • 배병선 지음
  • 426쪽
  • 28,000원
  • 2014년 05월 28일

일부 자바 개발자는 float과 double을 이용한 부동소수점 연산이 언제나 정확한 값을 도출한다고 믿는다. 하지만 그런 예상과 달리 float과 double은 정확한 값이 아닌 근사치를 계산하며, 그로 인해 개발자를 혼란에 빠트린다. 예제 1.7.1과 같이 4.7 + 0.4의 결과는 5.1이어야 정확하지만, 실제 결과는 근사치인 5.1000000000000005로 나타난다. 이런 문제를 해결하려면 비록 속도가 느리더라도 정확한 값의 계산이 필요할 경우 BigDecimal을 사용한다. (BigDecimal에 관한 자세한 내용은 2.1.11, “BigDecimal의 함정”에서 설명한다.)

예제 1.7.1 부정확한 부동소수점 연산의 결과

package com.software.basic.problem;

public class FloatExample3 {
    public static void main(String[] args) {
        double total = 0;
        total += 4.7;
        total += .4;
        System.out.println(total);
    }
}
실행결과
5.1000000000000005

부동소수점 연산 문제의 연장선으로 for 문의 증감변수에 float을 사용하는 경우가 있다. 즉, 일반적으로 for 문의 증감변수에는 int 타입의 변수를 사용하는데, 수치 계산 또는 외환 계산을 위해 for 문에 증감 변수로 float 타입을 사용할 때도 예제 1.7.1과 같은 문제가 발생한다. 이는 개발자가 예상하지 못하는 버그를 만들어낼 수 있으며 매우 혼란스러운 코딩 방법이다. 예제 1.7.2를 실행해 문제점을 확인해 보자.

예제 1.7.2 for 문에 float 형 증감변수를 사용한 예

package com.software.basic.problem;

public class FloatExample {
    public static void main(String[] args) {
        final int START = 2000000000;
        int count = 0;
        for (float f = START; f < START + 50; f++) {
            count++;
        }
        // 아래의 결과는 몇 일까? 50?
        System.out.println(count);
    }
}

예제 1.7.2의 결과는 우리가 예상한 50일까? 이 예제를 직접 실행해 보면 그림 1.7.1과 같이 우리의 예상을 크게 벗어난 0이 출력된다.

figure1-7-1
그림 1.7.1 예제 1.7.2를 실행한 결과

분명 프로그램의 설계상 for 문에서 float 형 f를 int 형 START의 값인 2,000,000,000으로 초기화했고, START에 50을 더한 값을 반복 조건으로 설정했으며, 증감식은 f++로 f에 1씩 증가하게 돼 있다. 그리고 이 반복문이 실행되면 증감변수가 증가할 때마다 count도 1씩 증가하게 돼 있다. 하지만 여기엔 함정이 하나 있다. int 형의 2,000,000,000과 2,000,000,050은 분명 다른 값이지만 이 값을 float 형으로 변환하면 둘은 같은 값이 되는 것이다.

조금 더 자세히 살펴보면 예제 1.7.3의 코드를 실행한 그림 1.7.2의 결과에서 나타나듯이 두 값 모두float에서는 2.0E9로 같은 값으로 인식한다. 그러므로 예제 1.7.2의 for 문은 실행과 동시에 반복조건을 만족하므로 종료되어 count는 초깃값 그대로 0이다. 이런 문제가 발생하는 원인도 float과 double의 부동소수점 연산이 정확한 값이 아닌 근사치를 구한다는 점에서 발생한다.

예제 1.7.3 float 부동소수점 연산의 문제점

package com.software.basic.problem;

public class FloatExample2 {
    public static void main(String[] args) {
        // 자바 7에서는 아래와 같이 숫자 사이에 언더바로 자릿수를 구분할 수 있다.
        // 하지만 자바 7 이전 버전에서는 적용되지 않는다.
        int value1 = 2_000_000_000;
        int value2 = 2_000_000_050;

        float fValue1 = 2_000_000_000;
        float fValue2 = 2_000_000_000;
        // int 형의 value1과 value2는 다른 값이다.
        System.out.println("int 형 " + value1 + "은 "+ value2 +"와 " + (value1 == value2 ? "같다" : "다르다"));

        System.out.println("float 형 " + fValue1 + "은 "+ fValue2 +"와 " + (fValue1 == fValue2 ? "같다" : "다르다"));
    }
}
figure1-7-2
그림 1.7.2 예제 1.7.3을 실행한 결과

문제점 진단

앞에서 설명한 바와 같이 float 형 값을 for 문의 증감변수로 사용하는 것은 매우 위험한 코딩 스타일이며, 이를 방지하기 위해 PMD에는 DontUseFloatTypeForLoopIndices 룰이 존재한다. 이 룰은 그림 1.7.3과 같이 for 문의 증감변수가 int가 아닌 float으로 사용됐는지 진단하고 float으로 사용했을 경우 수정하길 권고한다.

figure1-7-3
그림 1.7.3 DontUseFloatTypeForLoopIndices 룰을 이용한 문제점 진단

해결 방안

이런 문제 또한 해결 방안은 매우 간단하다. 예제 1.7.4와 같이 단순히 for 문의 증감변수로 int 형만을 사용하거나 int의 범위가 넘어서는 큰 숫자일 경우 long을 사용하는 것이다.

예제 1.7.4 올바른 for 문의 증감변수 사용법

package com.software.basic.solution;

public class FloatExample {
    public static void main(String[] args) {
        final int START = 2_000_000_000;
        int count = 0;
        // int 또는 long을 사용하자.
        for (int f = START; f < START + 50; f++) {
            count++;
        }
        System.out.println(count);
    }
}