60. 정확한 답이 필요하다면 float와 double은 피하라

📝 아이템 60: 정확한 답이 필요하다면 float와 double은 피하라

🔹 핵심 요약

✅ float와 double은 근사치 계산에 적합하며 정확한 결과가 필요할 때는 적합하지 않음 ✅ 금융 계산 같은 정확한 계산에는 BigDecimal, int 또는 long을 사용해야 함 ✅ BigDecimal은 정확하지만 사용이 불편하고 느림 ✅ 성능이 중요하고 소수점을 직접 관리할 수 있다면 int나 long을 권장


📚 필수 개념 정리

🧮 부동소수점 vs 고정소수점 vs BigDecimal 비교

특성
부동소수점(float/double)
고정소수점(int/long)
BigDecimal

정밀도

제한적 (IEEE 754 표준)

소수점 직접 관리 (정수)

임의 정밀도 (무제한)

표현 범위

매우 넓음 (±1.7E±308)

제한적 (int: ~21억, long: ~922경)

사실상 무제한

정확성

부정확 (근사값)

정확 (범위 내에서)

정확 (십진수 표현)

성능

매우 빠름

빠름

느림 (~10배)

메모리

적음 (4/8바이트)

적음 (4/8바이트)

많음 (가변)

사용 편의성

쉬움

소수점 관리 필요

편리하지만 코드 복잡성 증가

적합한 용도

과학/공학, 센서값 계산

임베디드, 간단한 금융 계산(소규모)

금융/정밀 수치 계산

오차 발생

필연적

없음 (적절히 구현 시)

발생하지 않음

반올림 제어

제한적

직접 구현 필요

세밀한 제어 가능

이 코드의 결과는?

public class FloatingPointError {
    public static void main(String[] args) {
        double funds = 0;
        for (int i = 0; i < 10; i++)
            funds += 0.1;

        System.out.println(funds);
        System.out.println(funds == 1.0);
    }
}

→ 결과는 0.9999999999999999 \n false

👉 왜 이런 결과가 나올까?

  • 부동소수점의 한계: 0.1과 같은 일부 십진수는 컴퓨터에서 정확한 이진수로 표현되지 못해 작은 오차가 발생

  • 오차 누적: 아무리 오차가 작더라도 10번의 덧셈 과정에서 누적되어 최종 결과가 1.0이 아닌 0.999999...로 계산됨

💡 부동소수점의 한계

  • float와 double은 IEEE 754 부동소수점 방식을 사용

  • 2진수로 실수를 표현하기 때문에 0.1과 같은 십진수를 정확히 표현할 수 없음

  • 근사값으로 계산되어 금융 계산 등에서 오차 발생 가능

  • 걸프전 당시 미군의 패트리어트 미사일 시스템이 미사일 요격에 실패

  • 원인은 0.1초를 이진 부동소수점으로 정확히 표현하지 못한 오차

  • 시스템 가동 시간이 길어질수록 누적된 오차가 커졌고, 결과적으로 스커드의 위치 예측이 약 500m 이상 빗나가 요격 실패


🚨 잘못된 금융 계산 예시

float/double을 사용한 금액 계산

/**
 * double을 사용한 금융 계산의 문제점
 * 정확한 값이 계산되지 않아 오류 발생
 */
public class BrokenCashRegister {
    public static void main(String[] args) {
        double price = 2.10; // 상품 가격
        double payment = 1.05; // 지불한 금액

        // 거스름돈 계산 - 문제 발생!
        System.out.println("받아야 할 금액: " + (price - payment)); // 1.0499999999999998

        // 1.05가 나와야 하지만 부동소수점 오차로 인해 다른 값이 출력됨
    }
}

BigDecimal을 사용한 정확한 계산

import java.math.BigDecimal;

/**
 * BigDecimal을 사용한 정확한 금융 계산
 * 정확한 값을 계산할 수 있으나 코드가 장황해짐
 */
public class AccurateCashRegister {
    public static void main(String[] args) {
        // String 생성자를 사용해 정확한 값 표현
        BigDecimal price = new BigDecimal("2.10");
        BigDecimal payment = new BigDecimal("1.05");

        // 정확한 거스름돈 계산
        System.out.println("받아야 할 금액: " + price.subtract(payment)); // 정확히 1.05 출력
    }
}

⚠️ BigDecimal 사용 시 주의: double 값으로 BigDecimal을 생성하면 이미 오차가 있는 값이 전달됨 ▶️ String으로 생성

// 잘못된 방법 - 이미 오차가 있는 double 값으로 생성
BigDecimal wrongWay = new BigDecimal(0.1); // 0.1000000000000000055511151231257827021181583404541015625

// 올바른 방법 - 문자열로 생성하여 정확한 값 표현
BigDecimal rightWay = new BigDecimal("0.1"); // 정확히 0.1

💼 BigDecimal의 장단점

👍 장점

  • 정확한 실수 계산이 가능해, 금융 계산 등에 적합함

  • 반올림, 올림, 내림 등을 RoundingMode를 통해 세밀하게 제어할 수 있음

  • 정밀도(precision) 를 자유롭게 설정 가능하여 오차 없는 계산 가능

👎 단점

  • +, -, *, / 같은 기본 연산자 사용 불가 → add(), subtract() 등 메서드로만 연산 가능

  • 기본 자료형보다 성능이 느림 (약 10배 정도 느리다고 알려져 있음)

  • 일반적으로 기본 자료형에 비해 더 많은 메모리 사용량이 필요하고, 정밀도를 직접 수정할 수 있어 정밀도가 높으면 메모리 사용량이 많이 증가함

🔧 활용 예시: 할인율 계산

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * BigDecimal을 활용한 실전 할인율 계산 예시
 */
public class DiscountCalculator {
    public static void main(String[] args) {
        // 상품 가격과 할인율
        BigDecimal price = new BigDecimal("19.99");
        BigDecimal discountRate = new BigDecimal("0.15"); // 15% 할인

        // 할인액 계산 (가격 × 할인율)
        BigDecimal discountAmount = price.multiply(discountRate);

        // 최종 가격 계산 (가격 - 할인액)
        BigDecimal finalPrice = price.subtract(discountAmount);

        // 소수점 둘째 자리까지 반올림 (HALF_UP: 5 이상이면 올림)
        finalPrice = finalPrice.setScale(2, RoundingMode.HALF_UP);

        System.out.println("상품 원가: $" + price);
        System.out.println("할인액: $" + discountAmount);
        System.out.println("최종 가격: $" + finalPrice);
    }
}

🔄 int/long을 사용한 방법

🔢 단위 변환 접근법

금액을 계산할 때 달러나 원화 같은 단위 대신 센트나 전 단위로 계산하는 방법

/**
 * 주식 거래 시뮬레이션
 * 주식 가격을 센트 단위로 계산
 * 출력 시에만 실수로 변환, 계산은 모두 정수로 수행
 */
public class StockTrader {
    public static void main(String[] args) {
        // 센트 단위로 계산
        long stockPriceInCents = 12345; // $123.45
        int sharesCount = 100;

        // 총 비용 계산
        long totalCostInCents = stockPriceInCents * sharesCount;

        // 결과 출력 (센트 → 달러 변환)
        System.out.println("주당 가격: $" + stockPriceInCents / 100.0);
        System.out.println("총 주식 수: " + sharesCount + "주");
        System.out.println("총 비용: $" + totalCostInCents / 100.0);

        // int 사용 시 주의: 오버플로우 가능성 검사
        int maxIntValue = Integer.MAX_VALUE; // 2,147,483,647
        System.out.println("int 최대값: " + maxIntValue);
        System.out.println("큰 금액은 int 범위를 초과할 수 있어 long 사용 권장");
    }
}

📊 성능 비교

/**
 * BigDecimal과 long의 성능 비교
 */
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;

public class PerformanceComparison {
    public static void main(String[] args) {
        int iterations = 10_000_000;

        // long을 사용한 계산 시간 측정
        Instant start1 = Instant.now();
        long sum1 = 0;
        for (int i = 0; i < iterations; i++) {
            long value = i * 100; // 달러 → 센트 변환
            sum1 += value;
        }
        Instant end1 = Instant.now();

        // BigDecimal을 사용한 계산 시간 측정
        Instant start2 = Instant.now();
        BigDecimal sum2 = BigDecimal.ZERO;
        BigDecimal multiplier = BigDecimal.valueOf(100);
        for (int i = 0; i < iterations; i++) {
            BigDecimal value = BigDecimal.valueOf(i).multiply(multiplier);
            sum2 = sum2.add(value);
        }
        Instant end2 = Instant.now();

        // 소요 시간 비교
        long longDuration = Duration.between(start1, end1).toMillis();
        long bigDecimalDuration = Duration.between(start2, end2).toMillis();

        System.out.println("long 연산 소요 시간: " + longDuration + "ms");
        System.out.println("BigDecimal 연산 소요 시간: " + bigDecimalDuration + "ms");
        System.out.println("BigDecimal은 long보다 약 " +
                          (bigDecimalDuration / longDuration) + "배 느림");
    }
}
long 연산 소요 시간: 18ms
BigDecimal 연산 소요 시간: 423ms
BigDecimal은 long보다 약 23배 느림

[Execution complete with exit code 0]

🎯 언제 어떤 타입을 사용할까?

🧠 선택 가이드

상황
권장 타입
이유

금융 계산

BigDecimal

정확한 계산이 필수적인 경우

고성능 필요

int/long

성능이 중요하고 소수점 관리가 가능할 때

9자리 이하의 소수점

int

정확한 계산 + 성능 (overflow 주의)

18자리 이하의 소수점

long

정확한 계산 + 성능 (더 큰 범위)

과학/공학 계산

double

정확도보다 넓은 범위의 근사치 계산이 필요할 때

간단한 근사치 계산

float/double

정확한 값이 필요 없는 일반 계산


🎯 결론

📍 금융 계산 같은 정확한 결과가 필요할 때는 float나 double을 피하라

📍 정확한 답이 필요하면 BigDecimal, int 또는 long을 사용하라

📍 성능, 편의성, 정확성 중 우선순위를 정하고 적합한 타입을 선택하라

📍 BigDecimal은 정확하지만 사용이 불편하고 느림을 인지하라

📍 소수점을 직접 관리할 수 있고 성능이 중요하다면 int나 long을 선택하라


💭 느낀 점

💡 금융 계산에서 왜 BigDecimal이 필수적으로 사용되는지, 그리고 실무에서 얼마나 중요한지 체감하게 됨

💡 프로그래밍 언어의 특성과 자료형의 한계를 이해하고 적절한 상황에 맞는 도구를 선택하는 것이 얼마나 중요한지 다시 한번 생각해 보게 됨

Last updated