Java 실무: 할인 금액은 언제 반올림해야 할까
문제에서 시작하기
주문 화면에는 0.05짜리 상품 세 개가 있다. 할인율은 10%다. 각 상품의 할인 후 금액을 먼저 소수 둘째 자리로 반올림하면 합계는 0.15지만, 전체를 먼저 더하고 할인하면 0.14가 된다. 계산기가 틀린 것이 아니라 반올림 시점이라는 업무 규칙이 다르다. 금액 타입을 바꾸는 것만으로 정산 문제가 해결되지 않는 이유를 작은 주문으로 확인한다.
그림. 항목별 계산과 주문 전체 계산을 나란히 비교했다. 어느 쪽을 사용할지는 결제·정산 계약으로 결정한다. Devin.KR이 직접 제작한 학습용 SVG 개념도입니다. 이미지를 선택하면 크게 볼 수 있습니다.
학습 목표와 선수지식
문자열과 반복문을 이해하고 있다고 가정한다. 문자열에서 정확한 십진수를 만들고, 통화의 자릿수와 반올림 시점을 코드로 명시하며, 수치 비교와 표현 비교를 구분하는 것이 목표다.
실행 환경
JDK 17 이상. javac MoneyRounding.java && java MoneyRounding. 가상의 소수 둘째 자리 통화이며 실제 결제 연동은 없다.
직접 실행하기
MoneyRounding.java에 저장한다.
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
public class MoneyRounding {
static BigDecimal cents(BigDecimal value) {
return value.setScale(2, RoundingMode.HALF_UP);
}
public static void main(String[] args) {
var prices = List.of(new BigDecimal("0.05"),
new BigDecimal("0.05"), new BigDecimal("0.05"));
var rate = new BigDecimal("0.90");
var itemTotal = prices.stream().map(p -> cents(p.multiply(rate)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
var orderTotal = cents(prices.stream().reduce(BigDecimal.ZERO,
BigDecimal::add).multiply(rate));
System.out.println("item=" + itemTotal);
System.out.println("order=" + orderTotal);
if (orderTotal.compareTo(new BigDecimal("0.14")) != 0)
throw new AssertionError("rounding policy");
System.out.println("numericEqual=" +
(new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) == 0));
System.out.println("objectEqual=" +
new BigDecimal("1.0").equals(new BigDecimal("1.00")));
}
}
예상 결과
item=0.15
order=0.14
numericEqual=true
objectEqual=false
코드를 읽는 순서
금액 입력은 문자열에서 시작한다. 이 예제의 0.05는 이진 부동소수점 중간값을 거치지 않는다. 이미 double로 계산한 값을 마지막에 BigDecimal로 포장해도 앞 단계에서 잃은 정확도는 복원되지 않는다.
cents는 계산 전체에서 단 한 가지 정책을 드러내는 함수다. 항목별 청구가 계약이면 항목별 결과를 더하고, 주문 총액에 할인을 적용하는 계약이면 합계를 계산한 뒤 반올림한다. 어느 쪽이 항상 옳은지는 라이브러리가 결정하지 않는다.
compareTo는 여기서 숫자 크기를 비교한다. equals는 소수 자릿수도 고려하므로 1.0과 1.00은 같지 않다. 금액을 Map 키에 넣는다면 스케일을 일관되게 정규화하거나 금액과 통화를 가진 별도 값 객체를 설계해야 한다.
실제 서비스에서는 통화 코드와 허용 소수 자리, 음수 허용 여부, 최대 금액을 입력 경계에서 확인한다. 환불은 음수 금액만으로 표현하기보다 주문과의 관계 및 환불 가능한 잔액도 검증해야 한다. 계산 테스트와 업무 규칙 테스트가 둘 다 필요하다.
흔한 오류와 반례
new BigDecimal(0.1)은 문자열 생성자와 같은 의도를 보장하지 않는다. 원래 입력이 문자열이면 그대로 파싱한다.- 모든 통화를 소수 둘째 자리로 고정하면 안 된다. 이 함수는 이번 예제의 통화 계약에만 맞는다.
- 중간 단계마다 반올림하면 오차의 방향과 크기가 누적될 수 있다. 금액 표시용 포맷과 계산용 반올림을 혼동하지 않는다.
연습문제
각 상품을 0.15로 바꾸고 10% 할인한다. 항목별 반올림과 합계 반올림의 결과를 먼저 예측한 뒤 실행하라. 두 결과 중 어느 값을 주문 API 계약에 적어야 하는가?
정답과 해설
항목별로 0.135를 0.14로 반올림하므로 합계는 0.42다. 전체 0.45에 할인을 적용하면 0.405이므로 0.41이다. 선택은 정산 계약에 달려 있다. 선택한 정책을 API와 영수증에 일치시키고 이 두 경우를 회귀 테스트로 남긴다.
참고자료와 작성 정보
Devin.KR AI 작성 · 2026-09-12. 독립적으로 작성한 설명과 가상 데이터 예제다. 실행 결과와 경계 조건을 검증했으며, 공개 전 편집 검토 대상이다. 특정 서적의 번역·발췌·요약본이 아니다.