Home 컴퓨터의 수 표현 방식과 Decimal을 사용하는 이유
Post
Cancel

컴퓨터의 수 표현 방식과 Decimal을 사용하는 이유

글을 작성하게 된 계기


현재 회사의 정산 을 하며 돈을 다루다보니, BigDecimal을 자주 사용하고 있습니다. 그러다 보니 컴퓨터가 수를 표현하는 방식 과 Decimal에 대해 궁금해졌고, 알게 된 내용을 정리하기 위해 글을 작성하게 되었습니다.





1. 정수와 실수의 이진수 변환 및 표현


컴퓨터는 데이터를 이진수로 변환 하여 메모리에 저장합니다. BigDecimal을 왜 사용하는지 알기 전, 먼저 컴퓨터가 정수와 실수를 어떻게 표현하는지 에 대해 살펴보겠습니다.

  1. 정수
  2. 실수




1-1. 정수

컴퓨터는 정수를 표현할 때 몫이 0이 될 때까지 2로 나눈 후, 나머지를 기록합니다. 기록한 나머지는 역순으로 읽어 정수부의 이진수로 표현 됩니다. 예를 들어, 10진수 정수 15를 이진수로 변환하는 과정과 결과는 다음과 같습니다. 이를 통해 10진수 정수 15는 1111(2)로 나타낼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
   2|  15
      -----
   2|   7   ...1
      -----
   2|   3   ...1
      -----
   2|   1   ...1
      -----
         0  ...1



이를 8bit로 저장하면 메모리에 다음과 같이 저장됩니다.

1
| 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |



32bit로 저장하면 다음과 같고요.

1
2
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | ...... | 0 | 0 | 1 | 1 | 1 | 1 |
                              32 자리





1-2. 실수

반면 소수부를 이진수로 변환하려면, 소수부에 2를 곱하고 정수부를 기록 합니다. 소수부가 0이 될 때까지 반복하거나 원하는 자리수까지 계산한 뒤, 기록한 정수부를 순서대로 연결하면 소수부의 이진수 표현하는 것이죠. 예를 들어, 소수부 0.625의 이진수 표현은 0.101(2)입니다.

1
2
3
   0.625 × 2 = 1.25   → 정수부: 1, 소수부: 0.25
   0.25  × 2 = 0.5    → 정수부: 0, 소수부: 0.5
   0.5   × 2 = 1.0    → 정수부: 1, 소수부: 0.0 (종료)





1-3. 근사치

컴퓨터가 정수와 실수를 어떻게 나타내는지 살펴보았습니다. 그런데 10진수 실수는 모두 이진수로 정확히 변환되지 않습니다. 일부 실수는 변환 시 무한소수(Repeating decimal) 가 발생하기 때문입니다.

A repeating decimal or recurring decimal is a decimal representation of a number whose digits are eventually periodic; if this sequence consists only of zeros, the decimal is said to be terminating, and is not considered as repeating.



예를 들어, 10진수 0.1을 이진수로 변환하면, 0.0001100110011…처럼 반복되는 패턴이 나타납니다. 이는 이진수가 소수를 표현할 때 2의 거듭제곱으로 나누는 형태를 사용 하기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
   0.1 × 2 = 0.2   →  정수부: 0, 소수부: 0.2
   0.2 × 2 = 0.4   →  정수부: 0, 소수부: 0.4
   0.4 × 2 = 0.8   →  정수부: 0, 소수부: 0.8
   0.8 × 2 = 1.6   →  정수부: 1, 소수부: 0.6
   0.6 × 2 = 1.2   →  정수부: 1, 소수부: 0.2
   0.2 × 2 = 0.4   →  정수부: 0, 소수부: 0.4
   0.4 × 2 = 0.8   →  정수부: 0, 소수부: 0.8
   0.8 × 2 = 1.6   →  정수부: 1, 소수부: 0.6

   ......
   



패턴이 반복되기 때문에 영원히 끝나지 않죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   0.1
×    2
------
   0.2   ... 정수부: 0
×    2
------
   0.4   ... 정수부: 0
×    2
------
   0.8   ... 정수부: 0
×    2
------
   1.6   ... 정수부: 1
×    2
------
   1.2   ... 정수부: 1
×    2
------
   0.4   ... 정수부: 0
×    2
------
   0.8   ... 정수부: 0
×    2
------
   1.6   ... 정수부: 1

......



이런 특성 때문에 컴퓨터에서 실수를 다룰 때 근사 표현(Approximation) 이나 정밀도 손실(Loss of Precision) 이 발생합니다. 패턴이 반복될 경우, 컴퓨터는 bit 수를 제한해 일정 자리까지만 저장하는 근사치 를 사용하기 때문입니다.

Precision loss can occur with decimal and double data types in a calculation when the result produces a value with a precision greater than the maximum allowed digits.



예를 들어 소수부가 무한히 반복되는 경우, float(32bit)는 가수부 23bit, 지수부 8bit, 부호 1bit 를 사용해 컴퓨터는 제한된 가수부 bit 수에 맞춰 잘라서 저장합니다. 잘린 부분은 손실되며, 이는 계산 결과에 영향을 미칩니다. 부호(S) 0 (양수), 지수(E) 127−4=123 -> 01111011(2), 가수(M) 10011001100110011001100 이 되는 것이죠.

1
2
| S(1bit) | E(8bit)     | M(23bit)                  |
|   0     | 01111011    | 10011001100110011001100   |



double은 float보다 더 많은 bit를 사용하여 상대적으로 더 높은 정밀도를 제공합니다. 하지만 double도 float과 마찬가지로 유한한 bit 수로 실수를 표현하기 때문에 정밀도 손실이 발생할 수 있습니다.

1
2
| S(1bit) | E(11bit)     | M(52bit)                                           |
|   0     | 10000000100  | 1001100110011001100110011001100110011001100110011001



원리는 똑같은데요, 0.1을 double로 표현할 때, 10진법에선 깔끔하게 끝나는 수지만, 이진법에선 무한 반복 소수가 됩니다. 0.1 = 1/10 = 2 / (2×5)로, 분모에 2 이외의 소수인 5가 있어서요. 이진법은 분모가 2의 거듭제곱만 정확히 표현 가능하기 때문이죠.

1
0.1₁₀ = 0.0001100110011001100110011...(2진법, 무한 반복)



하지만 이 반복되는 이진수를 double은 52bit만으로 가수(fraction) 부분을 저장할 수밖에 없습니다. 즉, 무한 반복을 중간에서 끊어서 근사치로 저장하게 됩니다.

1
2
3
4
0.1 → 0x3FB999999999999A

# 실제 저장되는 값(10진수)
0.1000000000000000055511151231257827021181583404541015625





1-4. 정밀도 손실의 문제점

정밀도 손실은 결국 계산 누적 오류로 이어질 수 있습니다. float이나 double로 금액을 계산할 경우, 컴퓨터의 bit 제한과 소수 표현 방식의 한계 때문에 정밀도가 떨어지며, 특히 금액이 크거나 소수점 이하 정밀도가 중요한 상황에서는 오차가 누적되어 잘못된 계산 결과를 낳게 됩니다. 정산 과정에서 이런 문제가 발생하면, 거래 대사, 원장 조회, 디버깅 까지 하루 종일 붙잡고 있어야 할지도 모릅니다. 추가로 철야를 할지도 💩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class PrecisionLossExample {
    
   ......
   
   @Test
   void whenUsingDouble_thenMoneyMismatchOccurs() {
      final double amount = 1234.56;
      double total = 0.0;

      for (int i = 0; i < 9000; i++) {
         total += amount;
      }

      double expected = amount * 9000;

      log.info("double total     : {}", String.format("%.10f", total));
      log.info("expected (double): {}", String.format("%.10f", expected));
      assertNotEquals(expected, total);
   }
}
1
2
[main] INFO double total     : 11111039.9999999240
[main] INFO expected (double): 11111040.0000000000







2. 실수 표현 방식


이런 정밀도 문제를 해결하거나 완화하기 위해 고정 소수점(Fixed-Point)부동 소수점(Floating-Point) 과 같은 실수 표현 방식(Real Number Representation) 이 도입되었습니다. 실수 표현 방식은 컴퓨터가 소수점이 있는 숫자(실수)를 이진수로 저장하고 계산하는 방식으로, 소수점을 기준으로 숫자의 위치와 범위를 표현할 수 있도록 설계되었습니다.

  1. 고정 소수점
  2. 부동 소수점



2-1. 고정 소수점

고정 소수점 방식은 실수를 정수부와 소수부로 고정된 크기로 나누어 저장하는 방법입니다. 예를 들어, 16bit를 사용하는 경우, 8bit를 정수부에, 나머지 8bit를 소수부에 할당할 수 있습니다. 이렇게 bit의 크기를 고정하기 때문에 계산이 단순하고 빠르며, 정밀도를 일정하게 유지할 수 있습니다.

In computing, fixed-point is a method of representing fractional (non-integer) numbers by storing a fixed number of digits of their fractional part.



예를 들어, 12.375를 고정 소수점 방식으로 표현했을 때, 정수부는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
# 정수부 12 → 이진수 1100
2 | 12
  -----
2 |  6   ... 0
  -----
2 |  3   ... 0
  -----
2 |  1   ... 1
     0   ... 1



10진수 0.375를 이진수로 변환하면 다음과 같습니다.

1
2
3
0.375 × 2 = 0.75  → 정수부: 0, 소수부: 0.75
0.75  × 2 = 1.5   → 정수부: 1, 소수부: 0.5
0.5   × 2 = 1.0   → 정수부: 1, 소수부: 0.0



결과적으로 12.375는 메모리에 다음과 같이 저장됩니다: 이를 메모리 표현하면 다음과 같은데, 고정 소수점 방식에서는 정수부와 소수부를 각각 정해진 bit에 저장합니다.

1
2
| 부호 비트(1bit) | 정수부(8bit) | 소수부(8bit) |
|       0       |   00001100 |   01100000  |



2-1-1. 고정 소수점 방식의 장점

고정 소수점 방식의 특징은 다음과 같습니다.

  1. 정수부/소수부 비율 고정: 메모리를 정수부와 소수부로 나누어, 일부는 정수부를 저장하고, 일부는 소수부를 저장합니다. 정수부/소수부 각각이 표현할 수 있는 범위는 제한됩니다.
  2. 소수부 자릿수 고정: 소수부의 자릿수를 미리 정하기 때문에, 표현 가능한 소수의 정밀도가 고정됩니다. 이로 인해, 정밀도를 더 높이려면 소수부의 bit를 늘려야 하고, 반대로 소수부를 줄이면 표현 가능한 정수의 범위가 줄어드는 문제가 발생합니다.
  3. 부호 비트(Sign Bit): 고정 소수점 방식에서는 맨 좌측 1bit를 부호 bit로 사용하여 양수와 음수를 구분합니다. 0은 양수, 1은 음수를 나타냅니다.



2-1-2. 고정 소수점 방식의 단점

고정 소수점 방식은 정수부와 소수부의 bit 수를 미리 고정해서 실수를 표현하기 때문에, 구조적으로 몇 가지 뚜렷한 한계를 가지고 있습니다.



2-1-2-1. 표현 범위의 한계

고정 소수점 방식에서는 정수부와 소수부가 각각 사용할 수 있는 bit 수가 고정되어 있기 때문에, 표현할 수 있는 값의 범위 자체가 제한적입니다. 예를 들어, 다음과 같이 16bit 전체를 나누는 고정 소수점 형식을 가정해보겠습니다.

1
| 부호 비트(1bit) | 정수부(7bit) | 소수부(8bit) |



이 경우, 정수부는 7bit이므로 표현 가능한 정수의 범위는 다음과 같습니다. 즉, 최대 127까지의 정수만 표현 가능하므로, 500.875와 같은 값은 범위 초과로 인해 표현할 수 없습니다. 이런 제약은 고정 소수점 방식의 구조적인 특성 때문에 유동적인 숫자 범위를 요구하는 분야에서는 부적합할 수 있습니다.

1
2^7 - 1 = 127




2-1-2-3. 소수부의 bit 낭비

고정 소수점은 소수부의 정밀도 또한 bit 수로 고정합니다. 그런데 모든 소수가 복잡하지는 않기 때문에, 소수부가 단순한 값을 표현할 때는 bit가 낭비됩니다. 예를 들어, 102.125는 다음과 같이 이진수로 표현됩니다:

1
2
102       = 1100110 (2)
0.125     = 0.001 (2) = 1 × 2^-3



이 경우, 소수부 0.125는 단 3bit만으로 충분히 표현 가능합니다. 그러나 고정 소수점 구조상, 소수부는 항상 8bit로 고정되어 있으므로, 나머지 5bit는 데이터를 담고 있지 않더라도 무조건 할당됩니다. 이러한 방식은 데이터의 특성과 관계없이 동일한 공간을 사용하는 비효율성을 발생시킵니다.

1
소수부 비트: 00100000 (앞 3bit만 의미 있고, 뒤 5bit는 낭비)




2-1-2-2. 정수부의 bit 낭비

반대로, 어떤 값들은 정수부가 아주 작고 소수부가 매우 정밀한 경우도 있습니다. 예를 들어 3.999999라는 수는 다음과 같이 구성됩니다.

1
2
3         = 11 (2) → 2bit면 충분
0.999999  ≈ 복잡한 이진 소수 (무한 반복 → 많은 bit 필요)



이때, 정수부는 단 2bit로도 충분하지만, 고정 소수점에서는 정수부에 8bit를 무조건 예약합니다. 결국 6bit가 사용되지 않으면서도 메모리를 차지하게 됩니다. 이러한 구조는 정수/소수 비율이 데이터마다 다를 때 매우 비효율적입니다.

1
정수부 bit: 00000011 (앞 6bit는 낭비)



가장 근본적인 문제는, 고정 소수점 방식에서는 정수부와 소수부 간의 bit 분할을 유동적으로 조절할 수 없다는 점입니다. 예를 들어 어떤 숫자는 정수부보다 소수부에 더 많은 bit를 할당해야 하고, 반대로 소수부가 거의 없고 정수부가 큰 숫자도 있을 수 있습니다. 하지만 고정 소수점에서는 이러한 데이터 특성에 따라 정수/소수 bit를 조정하는 것이 불가능하므로, 일부 데이터는 표현이 불가능하고, 일부는 표현은 가능하나 비효율적인 공간 낭비를 유발하게 됩니다.





2-2. 부동 소수점

부동 소수점 방식은 정수부와 소수부를 고정하지 않고 소수점의 위치를 가변적으로 관리하여 넓은 범위를 표현하는 방식입니다. 공간 낭비를 줄이고 매우 크거나 작은 숫자를 효과적으로 표현할 수 있다는 장점이 있습니다. 현재 대부분의 컴퓨터는 IEEE 754 표준을 따릅니다. 부동 소수점 방식은 소수점의 위치를 고정하지 않고 가수(Mantissa)지수(Exponent)의 조합으로 실수를 표현합니다. 이 방식은 고정된 bit 내에서 훨씬 더 넓은 숫자 범위를 표현할 수 있으며, 작은 값과 큰 값을 모두 정밀하게 다룰 수 있는 유연성을 제공합니다. 현재 대부분의 시스템에서 사용하는 IEEE 754 표준은 이러한 부동 소수점 방식을 따릅니다.

1
±(1.M) × 2^(E - Bias)



부동 소수점 방식은 다음과 같은 구조로 이루어져 있습니다.

  • 1.M: 가수부(Mantissa)이며, 항상 1로 시작하는 정규화된 이진수입니다. 이 앞자리 1은 저장하지 않고 암묵적으로 존재한다고 가정합니다.
  • E: 는 지수부(Exponent)이며, Bias(편향값)를 뺀 결과로 실제 지수값을 결정합니다.
    • IEEE 754 단정도(float)는 Bias = 127
    • 배정도(double)는 Bias = 1023



부동 소수점 방식에서 실수 12.375를 단정도(float, 32bit) 형식으로 표현하기 위해 다음과 같은 과정을 거칩니다. 먼저, 정수부와 소수부를 각각 이진수로 변환합니다.

1
2
3
4
정수부:   12      = 1100
소수부:   0.375   = 0.011

→ 전체 이진수 표현: 1100.011 (2진수)



정규화 (Normalization). 부동 소수점은 정규화된 형태로 저장되므로, 소수점을 가장 앞의 1 뒤로 옮깁니다. 소수점이 세 칸 왼쪽으로 이동하므로 지수는 2^3이 됩니다.

1
2
3
4
정규화된 표현: 1.100011 × 2^3

가수(M): 100011
지수(E): 3



3단계: IEEE 754 형식으로 변환. IEEE 754 단정도는 32bit 구조를 가지고 있으며, 다음과 같이 구성됩니다.

1
| S(1bit) | E(8bit) | M(23bit) |



각 필드의 값을 채워보면 다음과 같습니다.

1
2
3
| S |    E (8bit)    |            M (23bit)          |
|---|----------------|-------------------------------|
| 0 |   10000010     | 10001100000000000000000       |



각 필드의 값은 다음과 같습니다. 부호 bit(Sign bit)는 값이 양수이므로 0이고, 지수(Exponent)는 정규화 과정에서 소수점이 세 자리 이동했기 때문에 3이며, IEEE 754 단정도 형식의 bias 값인 127을 더한 결과 130, 이진수로는 10000010입니다. 가수(Mantissa)는 정규화된 이진수 1.100011에서 앞의 1을 생략한 소수부 100011을 기준으로 하며, 나머지는 23bit를 채우기 위해 0으로 패딩한 10001100000000000000000이 됩니다.

  • 부호(S): 0 (양수)
  • 지수(E): 127 + 3 = 130 → 10000010
  • 가수(M): 10001100000000000000000



4단계: 최종 32bit float 이진 표현. 최종적으로 12.375를 float로 표현한 결과는 다음과 같습니다. 이 32bit 이진수는 메모리에 저장된 후, 부동 소수점 디코딩 과정을 통해 다시 12.375라는 실수로 해석됩니다.

1
| 0 | 10000010 | 10001100000000000000000 |





3. Decimal


부동 소수점과 고정 소수점은 2진수 기반의 한계와 bit 수의 제약으로 인해 정밀도 손실, 누적 오차, 반올림 오류 등의 문제를 가지고 있는데, Decimal은 이런 문제를 해결하기 위해 등장했습니다. 이는 10진수( Decimal) 기반의 수 표현 방식으로, 실수를 정확하게 표현하고 연산하기 위해 설계되었습니다.

The decimal numeral system is the standard system for denoting integer and non-integer numbers.



Decimal은 실수를 정수값 × 10의 거듭제곱 형태로 표현하며, 소수점의 위치는 Scale 값에 의해 조정됩니다. 실수를 정수와 소수부로 나누는 대신, 전체 숫자를 정수처럼 처리한 뒤, scale을 기준으로 소수점을 적절한 위치에 삽입하는 방식으로 동작하는 것이죠. 예를 들어, 값이 123.45인 Decimal은 다음과 같이 구성됩니다.

  • 내부 정수 값: 12345 → 정수로 다룸
  • 소수점 위치: 2자리 → scale = 2
  • 해석: 12345 × 10^(-2) = 123.45



이는 내부적으로 다음과 같이 계산되죠. 즉, 내부적으로는 아주 큰 정수 하나해당 정수에 곱해질 10의 음의 거듭제곱 지수 로 구성 됩니다.

1
2
실제 계산은: 12345 + 6789 = 19134  (정수끼리)
결과 표현은: 191.34   (scale = 2 이므로 소수점 둘째 자리 삽입)



3-1. Decimal의 장점

이 방식은 다음과 같은 실질적인 이점을 제공합니다. 이러한 특징 덕분에 Decimal은 정확성과 일관성이 중요한 계산에 널리 사용됩니다. 반올림 방식을 명시적으로 지정할 수 있어 회계나 금융처럼 정밀한 소수점 처리가 요구되는 분야에 적합합니다.

  1. 10진 실수 정확 표현: 부동 소수점은 0.1, 0.2 같은 10진수를 이진수로 변환할 때 무한소수로 근사하여 오차가 발생하지만, Decimal은 이를 정확하게 표현할 수 있어 정밀한 계산이 가능합니다.

  2. 정밀도 유지: Decimal은 사용자가 지정한 자릿수까지 정밀하게 값을 표현하고, 연산 결과에서도 오차 없이 계산됩니다. 예를 들어 0.1 + 0.2 = 0.3이라는 비교가 Decimal에서는 정확하게 참이 됩니다.

  3. 명시적 반올림 규칙 설정: Decimal은 반올림 방식을 명시적으로 설정할 수 있어, 회계·세금·금융 등에서 요구되는 엄격한 소수점 처리 규칙을 정확하게 구현할 수 있습니다.

  4. 일관된 계산 결과: 내부적으로 정수 기반 연산을 하므로, 동일한 계산을 수행했을 때 항상 같은 결과를 보장합니다. 이 점은 금융, 테스트 자동화 등에서 매우 중요한 요소입니다.




3-2. Decimal의 단점

  1. 연산 성능 저하: 내부적으로 정수 연산과 소수점 처리(scale 적용)를 별도로 수행하기 때문에, CPU에 내장된 부동 소수점 연산(float, double)보다 계산 속도가 느립니다. 특히 대량의 수치 연산이 필요한 경우 성능 차이가 뚜렷하게 나타날 수 있습니다.

  2. 메모리 사용량 증가: 같은 숫자라도 float(4byte), double(8byte)보다 더 많은 메모리를 사용합니다. Java의 BigDecimal은 내부적으로 정수값을 BigInteger로 저장하기 때문에 메모리 오버헤드가 큽니다.

  3. 하드웨어 가속 미지원: 대부분의 CPU는 float/double에 최적화된 연산 명령어를 제공하지만, Decimal 타입은 소프트웨어적으로 처리해야 하므로 연산 속도가 느릴 수밖에 없습니다.

  4. 복잡성: Decimal 타입은 반올림 모드, 스케일 지정, 문자열 기반 생성 등 다양한 옵션이 있어 초보자에게는 사용이 복잡할 수 있습니다. 실수로 scale을 다르게 설정하거나 반올림 모드를 명시하지 않으면 의도치 않은 결과가 나올 수 있습니다.







4. 내부 구성


Decimal은 정밀한 수치 연산을 위해 Precision, Scale, Rounding Mode 등의 개념을 사용하여 숫자를 표현하고 계산합니다. 현재 자바를 메인 언어로 사용하고 있는데, 자바를 기준으로 각 개념을 살펴보겠습니다.

  1. Precision
  2. Scale
  3. Rounding Mode



4-1. precision

정밀도(Precision)는 Decimal 숫자에서 전체 유효 숫자의 자릿수 를 의미합니다. 유효 숫자란, 숫자의 값에 영향을 미치는 모든 자릿수 를 말하며, 소수점의 위치와는 무관하게 숫자 자체가 갖는 의미 있는 자릿수 를 말합니다.

In mathematics, precision describes the level of exactness in a number’s digits.



이는 컴퓨터가 Decimal 수를 처리할 때 얼마나 많은 숫자를 정확하게 유지할 것인지를 결정하는 중요한 기준이 됩니다.

1
Precision = 정수부 + 소수부의 모든 유효 숫자 수 



예를 들어, 123.45라는 숫자는 소수점 앞의 123과 소수점 뒤의 45까지 포함해서 총 5자리의 유효 숫자를 가지고 있으며, precision은 5입니다. 정밀도가 높을수록 더 정교하고 오차 없는 계산이 가능하지만, 그만큼 메모리 사용량이 증가하고, 연산 비용이 커질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Decimal("123.45")
→ 유효 숫자: 1, 2, 3, 4, 5 → precision = 5

Decimal("0.0012")
→ 유효 숫자: 1, 2 → precision = 2 (소수점 앞의 0과 소수점 뒤에 나오며 숫자 앞에 오는 0은 유효 숫자가 아니다)

Decimal("100.00")
→ 유효 숫자: 1, 0, 0, 0, 0 → precision = 5 (뒤의 0도 의미를 가지므로 포함)

Decimal("0.00000000000000000001")
→ 유효 숫자: 1 → precision = 1

Decimal("3.14159265358979323846")
→ 유효 숫자: 총 21자리 → precision = 21





4-2. scale

스케일(Scale) 은 Decimal 숫자에서 소수점 이하의 자릿수 를 의미합니다. 즉, 소수점 기준으로 소수부에 해당하는 숫자의 개수를 나타내며, 소수점 이하의 자리수만 따로 저장둬서 실제 숫자는 정수처럼 다룰 수 있게 합니다.

Scale is the number of digits to the right of the decimal point in a number.



Decimal은 내부적으로 숫자를 정수(integer)로 저장하고, scale을 이용해 소수점 위치를 결정합니다.

실제 값 = 내부 정수 값 × 10^(-scale)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Decimal("123.45")
→ 소수점 아래 2자리 → scale = 2

Decimal("123.000")
→ scale = 3 (소수점 아래 0도 모두 포함)

Decimal("0.0012")
→ scale = 4 (0은 유효 숫자가 아니지만 scale에는 포함)

Decimal("3.14")
→ scale = 2

Decimal("100")
→ scale = 0 (소수점 없음)

Decimal("100.0")
→ scale = 1 (정수지만 소수 한 자리로 표현됨)

Decimal("100.00")
→ scale = 2 (정수지만 소수 둘째 자리까지 표현 → 정밀도 암시)





4-3. Rounding Mode

Rounding mode는 소수점 이하 숫자를 자를 때 반올림을 어떻게 할지 결정하는 규칙 입니다. Decimal 연산에서 scale 제한, 나눗셈, 또는 소수 자릿수 제한이 발생할 경우 반드시 반올림 규칙(Rounding Mode) 을 지정해야 합니다.

Rounding or rounding off means replacing a number with an approximate value that has a shorter, simpler, or more explicit representation.



Decimal 연산에서 정확히 떨어지지 않는 값이 나올 경우 어떻게 처리할지 명확하지 않으면 계산 결과가 애매하거나 예외가 발생하기 때문입니다. 1 ÷ 3 = 0.333... 으로 무한소수가 되니까요.

1
2
3
BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b);  // 예외 발생!

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.



주요 반올림 모드는 다음과 같습니다.

  • HALF_UP: 일반적인 반올림. .5 이상이면 올림, 미만이면 내림
  • HALF_EVEN: .5에서 가까운 짝수로 반올림 (Banker’s Rounding). 오차 누적 방지
  • UP: 무조건 올림. 양수/음수 상관없이 절댓값 증가
  • DOWN: 무조건 내림. 양수/음수 상관없이 절댓값 감소
  • CEILING: 양수는 올림, 음수는 내림
  • FLOOR: 양수는 내림, 음수는 올림
  • UNNECESSARY: 반올림 없이 정확히 떨어질 경우만 허용, 아니면 예외 발생



Rounding Mode는 상황에 따라 적절히 선택해야 합니다. 일반 소비자 대상의 금액 계산에서는 HALF_UP 방식이 가장 흔히 사용되며, 5를 기준으로 반올림합니다. 대규모 금융이나 세금 계산에서는 장기적인 오차 누적을 방지하기 위해 HALF_EVEN을 사용하는 것이 적절합니다. 절대값이 커지는 것을 피해야 할 경우에는 ROUND_DOWN 또는 FLOOR을 사용하며, 반대로 절대값 증가를 허용하거나 계산값을 보수적으로 크게 만들고자 할 때는 ROUND_UP이나 CEILING을 사용합니다. 마지막으로, 값이 정확히 나누어떨어져야 하며 반올림이 전혀 허용되지 않는 상황에서는 UNNECESSARY를 사용해야 합니다.

1
2
3
4
5
6
7
8
val value = BigDecimal("123.456")

val halfUp     = value.setScale(2, RoundingMode.HALF_UP)     // 123.46
val halfEven   = value.setScale(2, RoundingMode.HALF_EVEN)   // 123.46
val roundDown  = value.setScale(2, RoundingMode.DOWN)        // 123.45
val roundUp    = value.setScale(2, RoundingMode.UP)          // 123.46
val ceiling    = value.setScale(2, RoundingMode.CEILING)     // 123.46
val floor      = value.setScale(2, RoundingMode.FLOOR)       // 123.45







5. 정리


Decimal은 정밀한 수치 연산을 위해 설계된 데이터 타입으로, Precision, Scale, Rounding Mode 등의 개념을 통해 숫자를 표현하고 계산합니다. Decimal은 10진수 기반으로 실수를 정확하게 표현할 수 있어, 금융, 회계, 과학 계산 등에서 정확도를 위해 사용합니다. 정산할 때, 너무 자주 보는데요, 이젠 그만 보고 싶어요. 👻


This post is licensed under CC BY 4.0 by the author.