Study/Java
아이템 60. 정확한 답이 필요하다면 float와 double형의 사용을 피하라
monitor
2023. 1. 15. 20:47
Why?
정확한 계산을 위해서 부동 소수점의 사용을 피하여야 한다.
예를 들면, 금융 계산이 필요한 경우 굉장히 크리티컬한 이슈를 발생 시킬 수 있는데, 1달러 - 42센트를 계산하기 위해서
Java에서 계산하여 출력해보면
System.out.println(Double.valueOf(1-0.42));
출력 : 0.5800000000000001
이러한 오답을 내게 될 수 밖에 없다.
이유는 부동 소숫점 저장 방식에 따른 오류이다.
Reason
컴퓨터가 실수를 표현하는 방식이 2진수라서 정확한 소수의 표현이 되지 않는다.
컴퓨터가 정수 및 소수를 표현하는 방법은 위와 같다.
그래서, 0.75를 계산하려면 0.25 (1/4) + 0.5 (1/2)로 만드는 것인데 이는 위 방식대로라면 0.11으로 2진수로 표현이 가능하다.
하지만, 263.3의 경우 2진수로 표현시 100000111.010100110011.. 의 반복이 일어난다.
그럼 이것을 자바에서 저장하는 부동 소수점 방식으로 계산하면
100000111.010011001100110... -> 1.00000111010011001100110... 맨 앞에 1을 붙여 주고 계산
부호 비트(1bit) : 0 (양수)
지수 비트(8bit): (127 (뒤로 갈때 대비하여 0부터 시작) + 8) 2^8 승
가수 비트(23bit) : 00000111010011001100110
-> 저장 방식이 결국 가수 비트 23bit에서 멈추므로 누락이 발생 할 수 밖에 없다.
그래서, 정확한 계산을 위해서 저자는 int형이나 BigDecimal의 사용을 권장한다.
Code - Before
/**
* 두 지점 사이의 거리 계산
*
*@ param Integer vtxLon, Integer vtxLat, Integer resultLinkInfoLon, Integer resultLinkInfoLat
*@ return Double distance
*/
private Double getDistance(Integer nvtxLon, Integer vtxLat, Integer resultLinkInfoLon, Integer resultLinkInfoLat){
Double lonA = Double.valueOf(vtxLon) / LON_LAT_CONSTANT * PI / STRAIGHT_ANGLE;
//noinspection WrapperTypeMayBePrimitive
Double latA = Double.valueOf(vtxLat) / LON_LAT_CONSTANT * PI / STRAIGHT_ANGLE;
Double lonB = Double.valueOf(resultLinkInfoLon) / LON_LAT_CONSTANT * PI / STRAIGHT_ANGLE;
//noinspection WrapperTypeMayBePrimitive
Double latB = Double.valueOf(resultLinkInfoLat) / LON_LAT_CONSTANT * PI / STRAIGHT_ANGLE;
Double thetaAngle = acos(sin(latA) *sin(latB) + cos(latA) * cos(latB) * cos(lonB-lonA));
if(isNaN(thetaAngle * EARTH_RADIUS))
{
return 0.0;
}
else {
return thetaAngle * EARTH_RADIUS;
}
}
Code - After
public static final BigDecimal LON_LAT_CONSTANT = new BigDecimal(String.valueOf(3600.0));
//문자열로 지정해야 Double to BigDecimal 시 근소한 차이의 오차를 발생시키지 않으므로 문자열로 지정하였습니다.
public static final BigDecimal PI = new BigDecimal(String.valueOf(3.1415926535897));
public static final BigDecimal STRAIGHT_ANGLE = new BigDecimal(String.valueOf(100.0));
public static final BigDecimal LAT_LON_DIVIDE = (LON_LAT_CONSTANT.multiply(PI)).divide(STRAIGHT_ANGLE,11,BigDecimal.ROUND_CEILING);
//나눗셈의 경우 소숫점을 지정하지 않으면 BigDecimal의 한계 지점까지 도달 할 수 있으므로 끝을 지정하고 올림을 사용하였습니다.
private Double getDistance(Integer vtxLon, Integer vtxLat, Integer resultLinkInfoLon, Integer resultLinkInfoLat){
BigDecimal vtx = new BigDecimal(vtxLon);
Double lonA = vtx.divide(LAT_LON_DIVIDE,11,BigDecimal.ROUND_CEILING).doubleValue(); //Demical 사용하더라도 제약이 존재한다. 11자리수 부터 소수점 버리고 값 올리고 진행
vtx = new BigDecimal(vtxLat);
Double latA = vtx.divide(LAT_LON_DIVIDE,11,BigDecimal.ROUND_CEILING).doubleValue();
vtx = new BigDecimal(resultLinkInfoLon);
Double lonB = vtx.divide(LAT_LON_DIVIDE,11,BigDecimal.ROUND_CEILING).doubleValue();
vtx = new BigDecimal(resultLinkInfoLat);
Double latB = vtx.divide(LAT_LON_DIVIDE,11,BigDecimal.ROUND_CEILING).doubleValue();
// 그나마 BigDecimal 계산 이후 Double로 변환하여 어느정도 손실을 막을 수 있었습니다. (하지만, Double로 변환 됨에 따라서 근소한 손실 값 다시 존재)
Double thetaAngle = acos(sin(latA) *sin(latB) + cos(latA) * cos(latB) * cos(lonB-lonA));
Double AngleMultRadius = new BigDecimal(String.valueOf(thetaAngle)).multiply(new BigDecimal(String.valueOf(EARTH_RADIUS))).doubleValue();
if(isNaN(AngleMultRadius))
{
return 0.0;
}
else {
return AngleMultRadius;
}
}