Namsang LABS
Deep Dive · #settlement #rounding #python #kotlin

코드로 읽는 회계: 반올림은 정책이다

· Sangkyoon Nam

오전에 출근하자마자 슬랙 알림이 울렸다. 재무 담당자 K씨였다.

“A사 이번 달 정산금액이 세금계산서랑 1원 차이가 나는데, 혹시 뭐 바뀐 거 있어요?”

없다. 아무것도 바꾼 게 없다. 그런데 1원이 다르다.

오후 내내 파고들었다. 원인은 허무했다. 정수 나눗셈에서 소수점이 조용히 잘리고 있었다. 고치는 건 한 줄이었다. 그런데 데이터를 좀 더 들여다보니 진짜 문제가 기다리고 있었다.

#round(0.5) 가 항상 1이 되는 것은 아니다

우리가 학교에서 배운 반올림 — 5 이상이면 올리고, 4 이하면 버리는 방식, 사사오입(四捨五入) — 에서는 0.5는 1이 되어야 한다. Python 2까지는 그랬다.

# Python 2
round(0.5)  # → 1.0
round(1.5)  # → 2.0
round(2.5)  # → 3.0

하지만 Python 3에서 실행하면 달라진다.

# Python 3
round(0.5)  # → 0
round(1.5)  # → 2
round(2.5)  # → 2

0.5는 0이 되고, 2.5도 2가 된다. Python 3에서 대체 무슨 일이 있었던 걸까.

#Banker’s Rounding

이 동작에는 이름이 있다. Banker’s Rounding, 또 다른 이름으로는 Round Half to Even이다. 0.5처럼 정확히 중간값일 때, 무조건 올리는 대신 가장 가까운 짝수 쪽으로 반올림하는 방식이다.

  • 0.5 → 0 (가까운 짝수는 0)
  • 1.5 → 2 (가까운 짝수는 2)
  • 2.5 → 2 (가까운 짝수는 2)
  • 3.5 → 4 (가까운 짝수는 4)

사사오입은 건건이 보면 아무 문제가 없다. 0.5를 1로 올리는 것뿐이니까. 하지만 수백만 건 거래에 반복 적용하면 이야기가 달라진다. 0.5를 항상 1로 변환하다 보면 올림 편향이 조금씩 누적된다. 건수가 많을수록, 금액이 클수록 이 편향은 무시할 수 없는 오차가 된다.

이 문제를 규칙으로 정리한 것이 Banker’s Rounding이다. 0.5일 때 올리거나 내리는 횟수를 장기적으로 균등하게 분산시켜 편향을 상쇄한다. 1985년, IEEE 754 부동소수점 표준이 제정되면서 이 방식이 기본 반올림 모드로 채택됐다. 이후 대부분의 하드웨어와 언어가 이 표준을 따르게 됐다.

한 가지 재미있는 사실이 있다. “Banker’s Rounding”이라는 이름의 유래는 사실 불분명하다. 실제로 은행권의 공식 표준이었다는 증거가 없고, 오히려 과거 은행권에는 통일된 반올림 표준 자체가 없었다는 기록이 있다. 이름만 Banker’s지, 출처는 금융권이 아닐 수 있다.

한자로는 오사오입(五捨六入)이라고 부른다. 사사오입(四捨五入)이 “4는 버리고 5는 올린다”는 뜻인 것처럼, 오사오입은 “5는 버리고 6은 올린다”는 뜻이다. 정확히는 5일 때 무조건 버리는 게 아니라 짝수 쪽으로 처리한다는 의미지만, 한자 이름 자체는 그 방향성을 직관적으로 담고 있다.

Python 3가 이 방식으로 바꾼 이유도 같은 맥락이다. Python 2의 반올림은 “0에서 멀어지는 방향으로”, 즉 사사오입이었다. Python 3는 “가장 가까운 짝수로” 바꿨다. 통계적으로 더 공정하고, IEEE 754 표준에도 부합하기 때문이다. Python을 과학 계산과 데이터 분석에 많이 쓰는 만큼, 대량 연산에서의 편향 누적을 줄이는 방향을 택한 것이다.

Java와 Kotlin도 Math.round()는 사사오입이다. 차이가 생기는 건 정밀한 소수점 계산이 필요할 때 쓰는 BigDecimal에서다. BigDecimal은 반올림 방식을 명시하지 않으면 아예 실행을 거부한다.

BigDecimal("2.5").setScale(0)
// → ArithmeticException: Rounding necessary
// (0.5를 올릴지 버릴지 알 수 없으므로)

이건 버그가 아니라 설계다. BigDecimal은 반올림 방식을 개발자가 직접 명시하도록 강제한다. 그래서 RoundingMode를 반드시 함께 써야 한다.

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

BigDecimal("2.5").setScale(0, RoundingMode.HALF_UP)   // 3 — 사사오입
BigDecimal("2.5").setScale(0, RoundingMode.HALF_EVEN) // 2 — Banker's Rounding
BigDecimal("2.5").setScale(0, RoundingMode.DOWN)      // 2 — 무조건 버림

같은 2.5인데 어떤 RoundingMode를 고르느냐에 따라 결과가 달라진다. Python이 기본값을 정해놓은 것과 달리, Java와 Kotlin은 선택을 강제한다.

위에서 이야기한 것처럼 언어마다 결과가 다르다. 이건 버그가 아니라 각자의 설계 철학이다.

언어 / 방법기본 동작
Python 3 round()Banker’s Rounding (HALF_EVEN)
Java / Kotlin Math.round()사사오입 (HALF_UP)
Java / Kotlin BigDecimal명시 필수 — 생략하면 예외
JavaScript Math.round()사사오입 (HALF_UP)

JavaScript의 Math.round()는 엄밀히 사사오입이 아니다. 양수에서는 사사오입과 동일하게 동작하지만, 음수에서는 다르다. Math.round(-0.5)-1이 아니라 0을 반환한다. JavaScript의 반올림은 ”+∞ 방향으로 올림(Round Half toward Positive Infinity)“이기 때문이다. 정산 시스템에서 음수 금액(환불, 조정 등)을 다루는 경우 이 차이가 문제가 될 수 있다.

소수점이 흔하게 쓰이는 달러 기준 시나리오를 하나 구체적으로 살펴보자. N사는 광고 대행사에 월 광고비의 15%를 수수료로 정산한다. 이번 달 광고비는 $18,336.30 집행 되었다.

수수료 = $18,336.30 × 0.15 = $2,750.445

결과는 소수점 세 자리다. 달러는 소수점 두 자리까지 표현한다. 이제 이 0.5센트를 어떻게 처리할 것인가.

정책청구 금액
사사오입 (HALF_UP)$2,750.45
Banker’s Rounding (HALF_EVEN)$2,750.44
절사 (DOWN)$2,750.44

사사오입은 올린다. Banker’s Rounding은 앞자리 4가 짝수이므로 내린다. 절사는 무조건 버린다. 같은 금액인데 정책에 따라 결과가 달라진다.

$0.01. 하나만 보면 크지 않게 느껴진다. 그런데 클라이언트가 수십 개고, 매달 쌓이면 연간으로는 무시하기 어려운 숫자가 누적된다. 더 중요한 건, 어느 쪽이 맞는가라는 질문에 코드는 답을 줄 수 없다는 것이다. 로직에는 아무 문제가 없다. 하지만 이 차이가 누적되어 정산 시스템과 회계 장부가 맞지 않는 오류로 재무팀에 발견된다.

만약 결제 시스템과 정산 시스템이 각자의 반올림 정책을 가지고 있다면? 프론트와 백엔드가 다른 정책을 사용해서 사용자 콘솔과 어드민이 보여주는 금액과 API가 계산하는 금액이 달라진다면? 개별 코드와 단위 테스트만 본다면 오류는 없다. 하지만 보이지 않는 버그는 더더욱 증폭될 수 있다.

정산 시스템에서 반올림은 언어의 기본 동작이 아니라 논의해야 하는 정책이다. 어떤 방법을 쓸지는 계약서에 명시된 조건, 세금계산서 기준, 업계 관행이 결정한다. 이건 기술 스펙이 아니라 비즈니스 의사결정이고, 그 결정이 정책으로 코드에 반영되어야 한다. (가능하면 주석도 남기자.)

// 2024-03-15 재무팀 협의. 계약서 4.2조 기준.
val ROUNDING_MODE = RoundingMode.HALF_UP

이를 명시하지 않으면, 또다시 슬랙 메시지를 받을 수 있다. 그리고 이번엔 1원이 아닐 수 있다.

이 글을 작성하는 2026년, 반올림은 코드 밖에서도 뜨거운 이슈가 됐다. 2025년 11월, 미국에서 마지막 페니(1센트 동전)가 주조됐다. 페니 공급이 줄면서 현금 거래의 반올림 정책이 비즈니스와 법률 문제로 번지고 있다. Square는 현금 거래를 5센트 단위로 자동 반올림하는 기능을 POS에 내장했고, 맥도날드는 매장별로 현금 반올림 정책을 도입했다. 어떤 체인은 고객 유리하게 내리고, 어떤 체인은 올린다. 주(州)마다 대응도 다르다 — 인디애나는 반올림 법안을 통과시켰고, 코네티컷은 무조건 내림을 권고하고, 워싱턴은 비대칭 반올림 법안을 추진 중이다. 전자결제는 여전히 센트 단위까지 정확하게 처리되지만 현금에 대한 연방 차원의 통일 기준은 아직 없다. 반올림이 정책이라는 건, 코드 안에서만이 아니라 계산대 앞에서도 똑같이 적용되는 현실이다.

#참고 자료

  • Shopify Engineering — Bound to Round: 8 Tips for Dealing with Hanging Pennies → 반올림 편향, 이해관계자 커뮤니케이션, 코드 일관성까지 다루는 실무 가이드.
  • Founding Minds — Rounding Numbers in the Financial Domain → 빌링 프로젝트에서 반올림 전략을 잡아간 경험기. IFRS/GAAP 회계 표준 맥락까지 다룬다.
  • Alipay+ — Banker’s Rounding → Alipay+가 Banker’s Rounding을 채택한 이유와 표준 반올림 대비 편차 비교를 수치로 보여준다.
  • 지마켓 기술블로그 — BigDecimal A to Z → Java/Kotlin에서 BigDecimal과 RoundingMode를 실무에서 쓸 때 참고할 레퍼런스.

Share this post