금액 계산: 최소 단위 정수냐 10진 타입이냐, N빵의 1원
숫자 강좌 4. int(19.99*100) 은 1998, BigDecimal 2.0 과 2.00 은 다르고, bcmul 은 소수를 버렸다. 따로 반올림한 N빵은 32% 가 틀렸고 Decimal 비용은 2.7~21배.
시리즈: 숫자·금액 계산 강좌 4/6 (중급). 반올림에서 한 번 데인 뒤(3강), 가격 저장, 장바구니 합계, N빵 정산, 쿠폰 할인 안분, 해외 결제 환산을 '1원 안 맞음' 문의 없이 구현하고 싶은 개발자를 위한 글입니다. 다 읽으면 정수(최소 단위)와 10진 타입을 언제 쓰는지, Python·Java·C#·PHP·Swift 의 10진 타입이 각각 숨긴 함정, 그리고 1원도 잃지 않는 안분 함수를 갖게 됩니다.
금액 버그는 요란하지 않습니다. 환불이 1원 모자라고, PG사 정산 파일과 매출 집계가 안 맞고, 어느 계층에서 금액이 슬그머니 float 가 돼 있습니다. 이 글은 그런 문제를 고쳐야 하는 사람을 위해, 규칙마다 실제 출력으로 근거를 붙였습니다. 원화는 '전' 단위를 쓰지 않아 최소 단위가 1원이므로 원화 금액은 그대로 정수입니다. 하지만 해외 결제, 달러 단가, 사용량 기반 요금에는 소수가 끼어듭니다. 아래는 모두 Apple Silicon Mac(M1 Pro)에서 Python 3.9.6(비교용 3.12), Java 26, .NET 10.0.14, PHP 8.5.6(bcmath, ICU 78.3 intl), Swift 6.4.2, SQLite 3.51.0, macOS jsc 로 실행한 결과입니다.
float 가 금액 그릇으로 부적합한 이유
1강에서 본 것처럼 0.1 은 2진수로 정확히 표현되지 않습니다. 금액에서는 변환하거나 자르는 순간 그것이 '다른 금액'이 됩니다.
# float_money.py
print("sum of ten 0.10 =", sum([0.10] * 10))
print("int(19.99 * 100) =", int(19.99 * 100))
print("round(19.99 * 100) =", round(19.99 * 100))
print("0.07 * 100 =", 0.07 * 100)
sum of ten 0.10 = 0.9999999999999999
int(19.99 * 100) = 1998
round(19.99 * 100) = 1999
0.07 * 100 = 7.000000000000001
int() 는 절사이고 19.99 * 100 은 1999 보다 아주 조금 작아서, 19.99 달러가 1998 센트가 됐습니다. JavaScript 도 19.99*100 = 1998.9999999999998, SQLite 도 CAST(19.99*100 AS INTEGER) 가 1998 이었습니다. 여기서는 round() 로 바꾸면 통하지만, 그러면 모든 개발자가 모든 계층에서 그걸 기억한다는 전제가 필요합니다.
SQLite 에서는 의외의 점도 있었습니다. REAL 로 저장한 0.10 10행을 sum 하면 3.51.0 에서는 정확히 1.0 을 돌려줬습니다(Python 의 단순 sum 은 0.9999999999999999). 최근 SQLite 는 부동소수점 합계를 더 꼼꼼히 계산하는 듯합니다. 그렇다고 REAL 이 금액에 안전해지는 것은 아니고, 오차가 다른 곳에서 나타날 뿐입니다.
선택지 1: 최소 단위 정수
가장 단순하고 튼튼한 규칙은 모든 금액을 통화의 최소 단위 개수, 즉 정수로 저장하는 것입니다. 19.99 달러는 1999, 4,500원은 4500, 1,500엔은 1500. 덧셈·뺄셈·비교가 정확하고 빠르며 어느 언어, 어느 DB 에서나 결과가 같습니다.
통화별 소수 자릿수를 네 라이브러리에 물어봤습니다.
| 통화 | Java Currency |
.NET CultureInfo |
PHP NumberFormatter(ICU 78.3) |
jsc Intl |
|---|---|---|---|---|
| KRW | 0 | 0 (ko-KR) | 0 | 0 |
| JPY | 0 | 0 (ja-JP) | 0 | 0 |
| CLP | 0 | 0 (es-CL) | 0 | 0 |
| COP | 2 | 0 (es-CO) | 0 | 2 |
| USD / EUR / MXN | 2 | 2 | 2 | 2 |
| BHD | 3 | (확인 안 함) | (확인 안 함) | (확인 안 함) |
Java Currency.getDefaultFractionDigits() 는 ISO 4217 을 따릅니다(COP 2, BHD 3). .NET 과 PHP 는 1234.5 콜롬비아 페소를 소수 없이 $ 1.235 로, jsc 는 $ 1.234,50 으로 출력했습니다. 원화도 1234.5 를 .NET 과 jsc 는 ₩1,235, PHP 는 ₩1,234(짝수 반올림)로 냈습니다. 최소 단위 자릿수는 로케일 서식기에서 추측하지 말고, 통화 코드를 키로 한 자체 표를 두고 입력과 출력 양쪽에서 쓰세요.
문자열 입력이 한 번도 float 를 거치지 않게 하는 작은 변환 함수입니다.
# cents.py
from decimal import Decimal
MINOR = {"JPY": 0, "KRW": 0, "CLP": 0, "USD": 2, "EUR": 2, "MXN": 2}
def to_minor(text, currency):
"""'19.99' USD -> 1999; '1500' JPY -> 1500. Refuses more digits than the currency has."""
d = Decimal(text)
scaled = d.scaleb(MINOR[currency])
if scaled != scaled.to_integral_value():
raise ValueError(f"{text} has more decimals than {currency} allows")
return int(scaled)
def show(minor, currency):
return f"{Decimal(minor).scaleb(-MINOR[currency]):.{MINOR[currency]}f} {currency}"
print(to_minor("19.99", "USD"), to_minor("1500", "JPY"), to_minor("12.5", "EUR"))
print(show(1999 * 3, "USD"), show(4500 * 3, "KRW"))
try:
to_minor("100.5", "JPY")
except ValueError as e:
print("ValueError:", e)
1999 1500 1250
59.97 USD 13500 KRW
ValueError: 100.5 has more decimals than JPY allows
정수에도 한계가 있습니다(2강). 부호 있는 64비트 정수는 약 9.2 × 10^18 단위까지 담으므로 가격에는 충분하지만, JavaScript Number 가 정확한 것은 2^53 까지입니다. 금액을 JSON 으로 브라우저에 보낸다면 문자열로 보내거나 충분히 작게 유지하세요.
선택지 2: 10진 타입, 그리고 각자의 함정
원·센트 정수로는 10% 세율이나 0.0123원 같은 사용량 단가를 담을 수 없습니다. 그런 계산에는 10진 타입이 필요합니다. 언어마다 있고, 언어마다 함정이 있습니다.
Python Decimal: float 가 아니라 문자열로 만든다.
# decimal_traps.py
import decimal
from decimal import Decimal, ROUND_HALF_UP
print("Decimal(0.1) =", Decimal(0.1))
print("Decimal('0.1') =", Decimal("0.1"))
print("Decimal(0.1) * 3 == Decimal('0.3'):", Decimal(0.1) * 3 == Decimal("0.3"))
ctx = decimal.getcontext()
print("prec =", ctx.prec, "| rounding =", ctx.rounding)
print("Decimal(1) / 3 * 3 =", Decimal(1) / 3 * 3)
print("quantize 2.665 ->", Decimal("2.665").quantize(Decimal("0.01")),
"| HALF_UP ->", Decimal("2.665").quantize(Decimal("0.01"), ROUND_HALF_UP))
ctx.traps[decimal.FloatOperation] = True # make float leaks loud
try:
Decimal(19.99)
except decimal.FloatOperation as e:
print("FloatOperation trapped:", type(e).__name__)
Decimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
Decimal('0.1') = 0.1
Decimal(0.1) * 3 == Decimal('0.3'): False
prec = 28 | rounding = ROUND_HALF_EVEN
Decimal(1) / 3 * 3 = 0.9999999999999999999999999999
quantize 2.665 -> 2.66 | HALF_UP -> 2.67
FloatOperation trapped: FloatOperation
교훈은 세 가지입니다. Decimal(0.1) 은 float 오차를 충실히 복사한다. 기본 컨텍스트는 유효숫자 28자리의 짝수 반올림이라 반올림 방식을 안 주면 quantize 가 2.67 이 아니라 2.66 을 준다. 나눗셈은 여전히 나누어떨어지지 않는다. FloatOperation 트랩을 켜면 float 가 Decimal 에 섞여 드는 순간 예외가 납니다.
Java BigDecimal: equals 는 수치 동등성이 아니다.
equals: false compareTo: 0
HashSet size: 2 TreeSet size: 1
stripTrailingZeros equals: true
new BigDecimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(0.1) = 0.1
10 / 3 (scale 2, HALF_EVEN) = 3.33
new BigDecimal("2.0").equals(new BigDecimal("2.00")) 는 스케일까지 비교해서 false 입니다. 둘을 넣은 HashSet 은 크기 2, compareTo 를 쓰는 TreeSet 은 1 이었습니다. new BigDecimal(0.1) 은 float 오차를 복사하고, BigDecimal.valueOf(0.1) 은 가장 짧은 문자열을 거쳐 0.1 입니다. 스케일 없는 나눗셈은 예외로, new BigDecimal("10").divide(new BigDecimal("3")) 는 java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result., 1.005 에 반올림 모드 없이 setScale(2) 하면 Rounding necessary 가 났습니다.
C# decimal: 가장 순하지만 한계는 있다.
0.1 + 0.2 == 0.3 (double): False
0.1m + 0.2m == 0.3m: True
1m / 3m * 3m = 0.9999999999999999999999999999
2.0m == 2.00m: True, ToString: 2.0 vs 2.00, Equals: True
decimal.MaxValue = 79228162514264337593543950335
OverflowException: Value was either too large or too small for a Decimal.
decimal 은 값으로 비교하면서 출력용 스케일은 유지합니다. 넘치면 checked 밖에서도 항상 예외입니다(long 은 checked 가 없으면 -9223372036854775808 로 돌아갔습니다).
PHP bcmath: 문자열로 받고 문자열로 돌려주며, 기본 스케일은 0.
bcmul('19.99','3') = 59 <- default scale 0
bcmul('19.99','3',2) = 59.97
bcdiv('2','3',2) = 0.66 <- truncates
bcadd('1e3','1') = ValueError: bcadd(): Argument #1 ($num1) is not well-formed
BcMath\Number('19.99') * 3 = 59.97
스케일 인수를 빠뜨리면 소수가 조용히 사라집니다. bcmul('19.99', '3') 은 59 였습니다. bcdiv 는 반올림이 아니라 절사이고, 지수 표기는 거부됩니다. PHP 8.4 에서 추가된 BcMath\Number 클래스는 스케일을 유지하고 연산자도 쓸 수 있으며, 8.5.6 에서 59.97 을 돌려줬습니다.
Swift Decimal: 소수 리터럴은 먼저 Double 을 거친다.
Decimal(0.1) = 0.1
Decimal(19.99) = 19.98999999999999488
Decimal(string:"19.99")! * 3 = 59.97
Decimal(0.1) 은 0.1 로 출력돼 함정을 놓치기 쉽지만, Decimal(19.99) 는 19.98999999999999488, 3강의 Decimal(1.005) 는 1.0049999999999997952 였습니다. Decimal(string:) 을 쓰세요.
N빵과 쿠폰 안분에서 1원도 잃지 않기
100,000원을 세 명이 나누면 똑같은 금액이 나올 수 없습니다. 각자를 따로 반올림하면 1원이 사라지거나 생깁니다. 최대 잔여법으로 고칩니다.
# alloc.py
import random
def allocate(total, weights):
"""Split integer `total` (minor units) in proportion to `weights`.
Largest remainder method: the parts always add up to `total`."""
wsum = sum(weights)
parts = [total * w // wsum for w in weights] # floor of each share
left = total - sum(parts) # units still to hand out
by_rem = sorted(range(len(weights)), key=lambda i: (-(total * weights[i] % wsum), i))
for i in by_rem[:left]:
parts[i] += 1
return parts
def naive(total, weights):
"""Round each share half-up on its own (the usual first attempt)."""
wsum = sum(weights)
return [(2 * total * w + wsum) // (2 * wsum) for w in weights]
for total, w, label in [(10000, [1, 1, 1], "$100.00 in 3"),
(100000, [1, 1, 1], "100,000 won in 3"),
(1000, [480, 980, 1980], "1,000 yen discount over 480/980/1980"),
(500, [1, 1, 1, 1, 1, 1], "5.00 EUR over 6 lines"),
(7, [1, 1, 1], "7 cents in 3")]:
n = naive(total, w)
print(f"{label}: naive {n} sum {sum(n)} | allocate {allocate(total, w)} sum {sum(allocate(total, w))}")
rng = random.Random(42)
T, bad, worst = 100_000, 0, 0
for _ in range(T):
w = [rng.randrange(1, 10_000) for _ in range(rng.randrange(2, 8))]
total = rng.randrange(100, 1_000_000)
d = sum(naive(total, w)) - total
if d:
bad += 1; worst = max(worst, abs(d))
assert sum(allocate(total, w)) == total
print(f"random splits: {T}, naive total wrong in {bad} ({bad / T:.1%}), worst {worst} units; allocate exact in all")
$100.00 in 3: naive [3333, 3333, 3333] sum 9999 | allocate [3334, 3333, 3333] sum 10000
100,000 won in 3: naive [33333, 33333, 33333] sum 99999 | allocate [33334, 33333, 33333] sum 100000
1,000 yen discount over 480/980/1980: naive [140, 285, 576] sum 1001 | allocate [139, 285, 576] sum 1000
5.00 EUR over 6 lines: naive [83, 83, 83, 83, 83, 83] sum 498 | allocate [84, 84, 83, 83, 83, 83] sum 500
7 cents in 3: naive [2, 2, 2] sum 6 | allocate [3, 2, 2] sum 7
random splits: 100000, naive total wrong in 31958 (32.0%), worst 3 units; allocate exact in all
둘째 줄이 회식비 N빵입니다. 100,000원을 세 명이 100000 // 3 으로 나누면 33,333원씩, 합계 99,999원이라 1원이 증발합니다. 셋째 줄은 쇼핑몰에서 흔한 '1,000원 할인 쿠폰을 상품별로 안분'하는 상황으로(숫자는 같고 통화 이름만 다릅니다), 480·980·1,980 상품에 비례해 따로 반올림하면 할인 합계가 1,001 이 됐습니다. 약속보다 1 많이 깎아 준 셈입니다. 부분 환불 때 상품별 안분액이 합계와 안 맞으면 환불 금액을 정할 수 없습니다. 무작위 10만 건(시드 42)에서는 32.0% 가 합계와 어긋났습니다. 안분 함수는 매번 정확하고, 동점이면 앞 줄에 주므로 같은 입력은 항상 같은 결과를 냅니다.
정산 앱이라면 1원 더 내는 사람을 '첫 번째 사람(총무)'처럼 규칙으로 화면에 보여 주면 다툼이 없습니다. 3강에서 본 10원 단위 절사와 섞을 때도 순서가 중요합니다. 먼저 정수로 정확히 안분하고, 절사 같은 표시 규칙은 그다음 한 번만 적용합니다.
해외 결제 환산: 환율은 하나, 반올림은 한 번
환율은 10진 문자열로 보관하고, 10진 타입으로 환산하고, 대상 통화의 최소 단위로 한 번만 반올림합니다. 아래 환율은 가상의 값입니다.
# fx_krw.py
from decimal import Decimal as D, ROUND_HALF_UP
def q(x, unit="1"):
return x.quantize(D(unit), ROUND_HALF_UP)
RATE = D("1385.47") # invented USD -> KRW rate, kept as a decimal string
won = q(D("19.99") * RATE)
print("19.99 USD ->", D("19.99") * RATE, "->", won, "KRW; back ->", q(won / RATE, "0.01"), "USD")
print("float 19.99 * 1385.47 =", 19.99 * 1385.47)
once = q(D("19.99") * RATE * D("1.10")) # convert + 10% tax, round once
twice = q(q(D("19.99") * RATE) * D("1.10")) # round after converting, then again
print("with 10% tax: round once", once, "| round twice", twice)
items = [D("0.99")] * 100
print("100 x 0.99 USD: convert each line", sum(q(p * RATE) for p in items),
"| convert the total", q(sum(items) * RATE))
19.99 USD -> 27695.5453 -> 27696 KRW; back -> 19.99 USD
float 19.99 * 1385.47 = 27695.545299999998
with 10% tax: round once 30465 | round twice 30466
100 x 0.99 USD: convert each line 137200 | convert the total 137162
두 번 반올림하니 1원이 움직였습니다. 0.99 달러짜리 100줄을 한 줄씩 환산하면 137,162원이 아니라 137,200원으로 38원 차이가 났습니다. 줄마다 1371.6153 이 1372 로 올라갔기 때문입니다. 앱 내 결제나 해외 구독 매출을 원화로 집계할 때는 줄 단위 환산인지 합계 환산인지 정하고, 쓴 환율과 반올림 규칙을 거래와 함께 기록해 나중에 같은 숫자를 재현할 수 있게 하세요.
실측: 10진 타입은 얼마나 느린가
10진 타입을 꺼리는 흔한 이유는 속도입니다. 같은 작업을 세 언어에서 쟀습니다. 무작위 금액 100만 개(시드 42)에 10% 세금을 더해 센트 단위로 사사오입하고 합산. Python 은 5회, Java 와 .NET 은 7회의 중앙값, Apple Silicon Mac(M1 Pro).
# bench.py (Python 부분)
import random, statistics, time
from decimal import Decimal as D, ROUND_HALF_UP
N, REPS = 1_000_000, 5
rng = random.Random(42)
cents = [rng.randrange(1, 100_000) for _ in range(N)] # 0.01 .. 999.99
floats = [c / 100 for c in cents]
decs = [D(c).scaleb(-2) for c in cents]
CENT, RATE = D("0.01"), D("1.10")
def with_int(): return sum((c * 110 + 50) // 100 for c in cents)
def with_float(): return sum(round(x * 1.10, 2) for x in floats)
def with_dec(): return sum((d * RATE).quantize(CENT, ROUND_HALF_UP) for d in decs)
| 언어 | 최소 단위 정수 | 2진 float | 10진 타입 |
|---|---|---|---|
| Python 3.9 | 64.3 ms | 231.7 ms | 173.7 ms (Decimal) |
| Python 3.12 | 54.7 ms | 240.7 ms | 160.3 ms (Decimal) |
| Java 26 | 1.4 ms (long) |
1.6 ms (double) |
22.5 ms (BigDecimal) |
| .NET 10 | 0.9 ms (long) |
1.0 ms (double) |
19.3 ms (decimal) |
Python 에서는 C 로 구현된 Decimal 이 float 와 round() 조합보다 빨랐고 정수가 가장 빨랐습니다. Java 와 .NET 에서는 10진 타입이 정수의 16배, 21배였지만, 그래도 세금 계산 100만 줄이 약 20밀리초입니다. 웹 요청 한 번에는 티도 안 나고, 문제가 되는 건 수억 줄짜리 배치입니다. 저장은 정수로, 10진 타입은 필요한 계산에서만 쓰라는 이유가 하나 더 생겼습니다.
float 버전은 느리기만 한 게 아니라 틀렸습니다. 정확한 합계보다 Python 에서 124.95(12,495 줄이 0.01 낮음, 3강 참고), Java 에서 6.83, .NET 에서 7.31 적었습니다.
표현 방식 고르기
| 필요한 것 | 쓸 것 | 주의할 점 |
|---|---|---|
| 금액 저장·합산·비교 | 최소 단위 정수(BIGINT 열) |
자릿수는 통화 표로. JS Number 는 2^53 까지 |
| 세율, 소수가 있는 단가 | 문자열로 만든 10진 타입 | Decimal(float), 스케일, 기본 반올림 |
| N빵·안분·부분 환불 | 정수에 최대 잔여법 | 동점 배분 순서를 정해 둘 것 |
| 해외 결제 환산 | 10진 문자열 환율, 반올림은 한 번 | 환율과 규칙을 거래와 함께 기록 |
| 차트·추정·통계 | float 도 무방 | 결과를 청구로 되돌리지 말 것 |
흔한 실수 → 실제로 일어난 일 → 고치는 법
| 실수 | 실행에서 일어난 일 | 고치는 법 |
|---|---|---|
int(price * 100) |
int(19.99 * 100) → 1998 |
문자열에서 변환: to_minor("19.99", "USD") |
Decimal(0.1), new BigDecimal(0.1), Swift Decimal(19.99) |
0.1000000000000000055511151231257827021181583404541015625, 19.98999999999999488 | 문자열로 생성. Python 은 FloatOperation 켜기 |
BigDecimal 을 맵 키로 |
2.0 과 2.00 의 HashSet 크기 2 |
compareTo, 또는 저장 전에 setScale(2) |
스케일 없는 bcmul |
bcmul('19.99','3') → 59 |
항상 스케일을 넘기거나 BcMath\Number |
| N빵을 각자 따로 반올림 | 100,000원÷3 → 합계 99,999원. 무작위로는 32.0% 가 틀림 | 최대 잔여법 안분 |
| 무심코 줄마다 환산 | 0.99 달러 × 100줄 → 137,200원 vs 137,162원 | 줄 단위/합계 단위를 정하고 기록 |
| 로케일 서식기에서 자릿수 추측 | COP: Java·jsc 는 2, .NET·PHP 는 0 | ISO 코드 기준 자체 통화 표 |
솔직한 한계와 버전 메모
BcMath\Number는 PHP 8.4 에서 추가됐고 확인은 8.5.6 에서만 했습니다. 통화 자릿수 표는 PHP 의 ICU 78.3, .NET 10 에 포함된 ICU, Java 26 의 통화 데이터를 반영하며 다른 버전에서는 다를 수 있습니다.- 측정은 한 대의 기계, 한 가지 작업, 작은 N 입니다. Java 는 소스에서 바로 실행(
java Bench.java), .NET 은dotnet run -c Release. JIT 워밍업은 반복 실행의 중앙값으로 흡수했습니다. Java 와 .NET 의 금액은 각자의Random(42)로 만들어 합계가 Python 과 다릅니다. - SQLite 가
0.1010행 합계로 정확히1.0을 낸 것은 3.51.0 에서 관찰한 것뿐입니다. - 예제의 환율과 세율은 가상 또는 예시입니다. 회계·세무·법률 조언이 아니며, 단수 처리 규칙은 세무 담당자와 정하세요.
연습 문제
1. 0.07 달러 상품을 int(0.07 * 100) 으로 센트로 바꿉니다. Python 은 0.07 * 100 = 7.000000000000001 을 출력했습니다. 이 경우 int() 는 안전한가요? 같은 코드가 실패하는 입력을 본문에서 하나 찾아보세요.
2. 회식비 100,000원을 일곱 명이 allocate(100000, [1] * 7) 로 N빵합니다. 한 사람당 얼마이고, 1원 더 내는 사람은 누구인가요?
정답
1. 이 경우는 우연히 통합니다. 7.000000000000001 을 절사하면 7 입니다. 19.99 에서는 실패합니다. int(19.99 * 100) 은 곱이 1999 보다 살짝 작아 1998 이었습니다. 어떤 가격에선 되고 다른 가격에선 깨진다는 것 자체가 문자열에서 변환해야 하는 이유입니다.
2. 100000 // 7 = 14,285원씩 합계 99,995원, 남은 5원. 나머지가 모두 같으므로 동점 규칙에 따라 앞의 다섯 명에게 1원씩 줍니다. 다섯 명이 14,286원, 두 명이 14,285원, 합계 100,000원입니다(실행 결과 [14286, 14286, 14286, 14286, 14286, 14285, 14285]).
다음 강의
- 이전: 숫자 3/6: 반올림 규칙, 은행가 반올림, 부가세 절사
- 다음: 숫자 5/6: 로케일별 숫자 서식과 입력 파싱
- '최소 단위' 뒤에 있는 정수 한계: 2강, 정수 오버플로와 BigInt
- JSON 숫자가 도중에 타입을 바꾸는 이야기: PHP prepared statement 가 JSON 숫자 타입을 바꾼다, JavaScript 성능·메모리·숫자
- DB 의 금액 열: SQL 기초: SELECT, WHERE, ORDER BY
- 안분을 테스트로 고정하기: 단위 테스트 기초
- 전체 강좌: 프로그래밍 학습 로드맵
자주 묻는 질문
금액은 정수로 저장해야 하나요, Decimal 로 저장해야 하나요?
저장과 덧셈은 통화의 최소 단위 정수(원화는 1원, 달러는 센트)로 하고, 세율·잘게 쪼갠 단가·환율 환산처럼 자릿수가 더 필요한 계산에만 Python Decimal, Java BigDecimal, C# decimal 같은 10진 타입을 씁니다. 마지막에 한 번만 반올림해 정수로 돌립니다. 2진 float 는 쓰지 않습니다. Python 에서 int(19.99 * 100) 은 1998 이었습니다.
BigDecimal 에서 2.0 과 2.00 이 같지 않은 이유는?
BigDecimal.equals 는 값과 스케일(소수점 아래 자릿수)을 함께 비교하기 때문입니다. new BigDecimal("2.0").equals(new BigDecimal("2.00")) 는 false, compareTo 는 0 이었습니다. HashSet 에는 둘 다 남고 TreeSet 에는 하나만 남았습니다. 금액 비교는 compareTo 로 하고, 맵 키로 쓰려면 setScale 로 스케일을 맞추세요.
N빵(더치페이)에서 1원도 안 틀리게 나누는 방법은?
최소 단위 정수에 최대 잔여법을 씁니다. 모두의 몫을 내림하고, 남은 단위 수를 세고, 나머지가 큰 사람부터 1씩 더 줍니다. 100,000원을 세 명이면 33,334·33,333·33,333원입니다. 각자를 따로 반올림하는 방식은 무작위 10만 건 중 32.0% 에서 합계가 맞지 않았습니다.