처음 금액 표시를 수정할 때는 ToString("N2")면 충분하다고 생각했다. 대부분의 예제에서 소수점 두 자리로 보여 줬고 테스트 데이터도 그럴듯하게 나왔다. 그런데 여러 통화를 사용하는 계좌를 붙이자 어떤 값은 자릿수가 부족했고, 어떤 값은 불필요한 0이 붙었다.
더 큰 문제는 화면을 맞추려고 원본 값을 먼저 반올림했을 때였다. 표시에는 문제가 없어 보였지만 합계와 상세 값이 아주 조금 달라졌다. 소수점 한 자리 차이가 단순한 디자인 문제가 아니라 계산 결과의 신뢰 문제로 이어질 수 있었다.
계산값과 표시값을 섞고 있었다
당시 코드를 따라가 보면 숫자가 문자열로 바뀌었다가 다시 숫자로 변환되는 구간이 있었다. 화면 문구를 만들기 위한 포맷과 계산을 위한 값이 섞여 있으니 어디에서 정밀도가 사라졌는지 찾기 어려웠다.
// 계산에는 원본 decimal 값을 유지한다.
decimal total = rows.Sum(row => row.amount);
// 화면에 보여 줄 때만 통화 규칙을 적용한다.
string text = total.ToString($"N{currency.decimalPlaces}");
핵심은 어려운 수학이 아니었다. 원본 값은 끝까지 숫자로 유지하고, 사용자에게 보여 주는 경계에서만 문자열로 바꾸는 것이었다.
통화마다 규칙이 달랐다
모든 통화가 같은 자릿수를 쓰지 않았고, 상품에 따라 최소 변화 단위도 달랐다. 그래서 화면마다 임의로 N2, N3을 쓰는 대신 통화 정보가 포맷 규칙을 제공하도록 보는 편이 안전했다.
sealed class CurrencyRule
{
public string Code;
public int DecimalPlaces;
}
이렇게 단순한 규칙 객체라도 있으면 새 통화가 추가됐을 때 여러 UI 스크립트를 찾아다닐 필요가 줄었다. 어디에서 잘못된 자릿수를 정했는지도 한곳에서 확인할 수 있었다.
0도 여러 종류처럼 보였다
값이 정말 0인 경우, 아직 값이 오지 않은 경우, 계산할 수 없는 경우가 화면에서는 모두 0.00으로 보이기도 했다. 사용자는 결과가 0이라고 이해하지만 실제로는 로딩 중일 수 있었다. 숫자와 상태를 따로 들고 있어야 했다.
if (!result.hasValue)
amountText.text = "-";
else
amountText.text = Format(result.value, rule);
null이나 별도 플래그를 어떻게 쓸지는 프로젝트마다 다르지만, 값이 없다는 상태를 임의의 숫자로 대신하지 않는다는 원칙은 기억에 남았다.
작은 표시 규칙이 신뢰를 만든다
사용자는 내부 계산 과정을 보지 못한다. 화면에 나온 숫자만 보고 결과를 판단한다. 그래서 한 자리의 반올림, 마이너스 부호, 천 단위 구분도 기능의 일부였다.
이후 숫자 표시 오류를 만났을 때 폰트나 텍스트 컴포넌트부터 보지 않는다. 원본 자료형, 계산 시점, 반올림 시점, 통화별 표시 규칙을 차례대로 확인한다. 당시에는 소수점 수정 하나가 왜 오래 걸리는지 답답했지만, 작은 값일수록 경계를 명확히 해야 한다는 걸 배운 작업이었다.