자바스크립트 소수점 연산 시 정밀도 문제

Maliethy·2022년 12월 27일
0

javascript error

목록 보기
5/5

작업 중 금액의 소수점 표기 시 다음과 같은 이슈가 발생했다.

issue

API 요청 시 응답값이 다음과 같이 오는 경우,
금액의 단위가 소수점 2번째 자리까지 오는데 기획 요구사항에 따라 사용자에게는 이를 그대로 표시해주어야 하는 이슈가 발생했다.

ex)
request

Request URL: https://example.com/api/v1/adjustment/reports/hubs/example/basic?fromAt=1671494400000&toAt=1672099199999

response

transactions: [{
    category: 'HUB', type: 'HUB_FEE_FOR_ORDERED_DISTRIBUTOR', amount: 785.5, taxAmount: 78.5, totalAmount: 864,
  }, {
    category: 'HUB', type: 'HUB_FEE_FOR_ORDERED_DISTRIBUTOR_REFUND', amount: -1140, taxAmount: -114, totalAmount: -1254,
  }, {
    category: 'HUB', type: 'MARGIN_FOR_DISTRIBUTOR', amount: 3000, taxAmount: 0, totalAmount: 3000,
  }, {
    category: 'HUB', type: 'MARGIN_FOR_DISTRIBUTOR_REFUND', amount: -3000, taxAmount: 0, totalAmount: -3000,
  }, {
    category: 'HUB', type: 'MGMT_FEE_PER_ORDER_FOR_DISTRIBUTOR', amount: 27.31, taxAmount: 0, totalAmount: 27.31,
  }, {
    category: 'HUB', type: 'MGMT_FEE_PER_ORDER_FOR_DISTRIBUTOR_REFUND', amount: -23.26, taxAmount: 0, totalAmount: -23.26,
  }],

  balanceAmount: {
    openBalanceAmount: 62645280.25,
    closeBalanceAmount: 62644894.3,
  },

예컨대 62645280.25 라는 숫자를 62645280.25원 이라고 표시해주어야 한다.

solution

우선 서버에서 금액이 소수 두번째 자리까지 오는 이유는 JAVA에서는 부동소숫점 문제로 금액계산할 때는 BigDecimal을 쓰는데, 현재 서버에서 소수점 두번째 자리까지 금액이 오는 이유는 예를 들면 금액은 408.00 인데 세금이 10%면 40.80 이 되고 총 금액은 448.80 이 되는 구조라서라고 한다.

본론으로 돌아가 클라이언트에서 이 소수점을 계산하면 다음과 같이 정밀도 문제로 소수점 계산 시 오류가 발생한다. 아래 예에서처럼 27.31-23.26을 더했는데 결과가 4.05가 아니라 4.049999999999999로 계산된다.

그 이유는 다음과 같다.

자바스크립트(javaScript)뿐만 아니라 Java, C, Python, Oracle 에서도 계산 오류가 발생한다.

우리가 보통 계산 할때 사용하는 10진법과 달리 컴퓨터는 2진법으로 동작하는데,

몇몇 소수는 10진법에서 2진법으로 변환하는 과정에서 무한 소수가 되어버린다.

저장공간에 한계가 있는 컴퓨터는 무한 소수를 유한 소수로 바꾸게 되는데,

이 과정에서 미세한 오차가 발생해서 오류가 발생한 것이다. (정밀도 문제)

미세한 값들이 손실되거나 초과된다. (위에서  0000000000000004에 해당)
출처:
https://joonpyo-hong.tistory.com/entry/JS-소수점-계산-오류-해결하는-법-부동-소수점

클라이언트에서 보여주고 싶은 값은 4.05원 과 같이 소수 두번째 자리수에 화폐단위 ‘원’을 더해주는 것이다.

현재 허브컨트롤룸에서는 화폐단위를 붙여줄 때 [Intl.NumberFormat constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat)를 사용한 currency method를 사용하고 있다.

const currency = (value, options = {}) => {
  let number = 0;
  if (typeof value === 'number') {
    number = value;
  } else if (typeof value === 'string') {
    number = Number.parseInt(value, 10);
  }
  const lcCurrency = i18n.t('common.currency');
  // currencyDisplay: 'name' 일 경우 '대한민국 원' 으로 표현됨
  return new Intl.NumberFormat('ko-KR',
    {
      style: 'currency',
      currency: 'KRW',
      currencyDisplay: 'name',
      minimumFractionDigits: 0,
      ...options,
    }).format(number).replace('대한민국', '').replace(/ /g, '')
    .replace('원', lcCurrency);
};

여기에 options로 maximumFractionDigits:2 로 설정하면 소수점 둘째자리라면 둘째자리까지, 첫째자리라면 첫째자리까지 표시된다.

totalAmount: currency(openBalanceAmount, {
          maximumFractionDigits: 2,
        }),

결과 값은 예를 들어,

4.054.05원

4.54.5원

44원

profile
바꿀 수 있는 것에 주목하자

0개의 댓글