backendjava
정산 시스템에서 1원이 사라지는 문제 — Largest Remainder Method
✨ AI 요약
긴 글, 핵심만 먼저 — 이 글을 3줄로 정리해드려요.
조회 0
구독료 천만 원을 시청 시간 비율로 세 CP에게 나눠준다고 하자. 비율대로 곱하고 원 단위로 반올림하면 이런 일이 생긴다.
CP A: 3,333,333.33... → 3,333,333
CP B: 3,333,333.33... → 3,333,333
CP C: 3,333,333.33... → 3,333,333
합계: 9,999,999 (1원 증발)
정산에서 "배분 합계 = 원본 금액"은 깨지면 안 되는 불변식이다. 1원이라도 어긋나면 회계가 안 맞는다.
Largest Remainder Method
선거 의석 배분에도 쓰는 고전적인 방법이다.
- 각자 몫을 내림(floor) 으로 확정한다
- 남은 금액(잔여)을 계산한다
- 소수점 나머지가 큰 순서대로 1원씩 더 준다
public List<BigDecimal> distribute(BigDecimal pool, List<BigDecimal> weights) {
BigDecimal total = weights.stream().reduce(ZERO, BigDecimal::add);
var shares = weights.stream()
.map(w -> pool.multiply(w).divide(total, 10, RoundingMode.DOWN))
.toList();
var floors = shares.stream().map(s -> s.setScale(0, RoundingMode.DOWN)).toList();
long remainder = pool.subtract(floors.stream().reduce(ZERO, BigDecimal::add)).longValue();
// 나머지 큰 순으로 1원씩
var order = IntStream.range(0, shares.size()).boxed()
.sorted(comparing(i -> shares.get(i).remainder(ONE), reverseOrder()))
.toList();
var result = new ArrayList<>(floors);
for (int k = 0; k < remainder; k++) {
int i = order.get(k);
result.set(i, result.get(i).add(ONE));
}
return result;
}
테스트는 불변식으로
케이스별 기대값보다 속성을 검증하는 게 든든하다.
@Test
void 배분_합계는_항상_원본과_같다() {
var result = calculator.distribute(pool, weights);
assertThat(sum(result)).isEqualByComparingTo(pool);
}
반올림 대신 내림+잔여배분. 정산·포인트·쿠폰 분할 어디든 똑같이 적용된다.