Minssuy

0.1 + 1.1 == 1.2 는 True 이다.

Computer Science for Game Development
By Minssuy
Posted 2026/04/162026년 4월 16일 목요일 AM 12:00
7 min read1126 words
0.1 + 1.1 == 1.2 는 True 이다.

편견


이 질문은, 소수점을 다루는 개발자라면 한번씩은 접해보는 문제입니다.
많은 사람들이 말하길, 0.1 + 1.1 == 1.2False 라고 합니다.
그럼, 제가 질문을 살짝 바꿔 다시 여쭤보겠습니다.


0.1 + 1.1 == 1.2False확실한가요?


문제


본문은 Unity 엔진의 C# 문법을 기준으로 설명하겠습니다.

public class Test : MonoBehaviour
{
    float a = 0.1f;
    float b = 1.1f;

    void Start()
    {
        float c = a + b;
        Debug.Log(c == 1.2f); // result is true? false?
    }
}

잠시 스크롤을 멈추고, 코드를 보고 질문에 답해주시기 바랍니다.
위의 코드에서, Debug.Log(c == 1.2f) 의 출력은 TrueFalse 중에 어떤 것으로 나오게 될까요?

5초 정도 시간을 드리겠습니다.











충분히 생각해보셨나요? 이 내용을 아는 분께선 5초라는 시간도 길었을 지도 모르겠습니다.
그럼 직접 실행시켜보겠습니다.

Console Result is True

그림의 아래쪽을 보니, 콘솔창에 True 라고 적혀있습니다.
최소한 저는 소수 + 소수 는 제대로 동작하지 않는다고 배웠기 때문에, False 라고 예측했습니다.

그렇다면, 컴퓨터와 그렇게 알려준 사람들 중 누가 거짓말을 하고있는걸까요?


저장


이러한 일이 일어난 이유를 알기 위해선, 컴퓨터가 어떤식으로 소수를 저장하는지 먼저 알아야합니다.

소수는 float 라는 자료형으로 값을 저장할 수 있고, 4 Byte 크기를 가집니다.
float는 아래의 그림과 같이 RAM 에 부호, 지수부, 가수부라는 공간으로 나누어 저장합니다.

Float Ram (4 byte, 32 bit)

32 bit1 bit 는 값의 부호를, 8 bit 는 지수부, 남은 23 bit 는 가수부로 사용되는 모습을 확인할 수 있습니다. 부호는 알지만, 지수부와 가수부라는 어려운 단어를 보니 벌써부터 머리가 아픈 느낌이 듭니다.
머리와 눈이 조금 아플 수 있습니다만, 이것들을 순서대로 하나씩 알아보겠습니다.


예시


11.25 라는 소수가 있습니다. 이 숫자가 float 자료형에 어떻게 저장되는지 살펴보겠습니다.


1. 이진법 변환

현재 11.25 는 십진법으로 표기되어있습니다. 이것을 이진법으로 표현하겠습니다.

11÷2=515÷2=212÷2=101÷2=011011\begin{aligned} 11 \div 2 &= 5 \cdots 1 \\ 5 \div 2 &= 2 \cdots 1 \\ 2 \div 2 &= 1 \cdots 0 \\ 1 \div 2 &= 0 \cdots 1 \end{aligned} \quad \Rightarrow \quad 1011
0.25×2=0.500.5×2=1.01.01\begin{aligned} 0.25 \times 2 &= 0.5 \quad \Rightarrow 0 \\ 0.5 \times 2 &= 1.0 \quad \Rightarrow 1 \end{aligned} \quad \Rightarrow \quad .01
11.25(10)=1011.01(2)11.25_{(10)} = 1011.01_{(2)}

2. 정규화

가장 왼쪽에 있는 1 을 기준으로 하여, 바로 뒤에 점을 찍고, 옮긴 칸 수 만큼 지수를 곱합니다.
이 과정을 정규화 Normalization 라고 하며, 앞부분 1.01101 을 가수부, 를 지수부 라고 부릅니다.

1011.01(2)  =  1.01101×231011.01_{(2)} \;=\; 1.01101 \times 2^{3}
  • 가수부(Mantissa) : 실제 수치를 담고 있는 중요한 부분 1.01101
  • 지수부(Exponent) : 가수부에 얼만큼 곱할건지 결정하는 부분

3. 값 저장

가수부와 지수부로 분리하였으니, 이것을 램에 저장해보겠습니다.
첫번째 칸의 부호는, 양수일 땐 0 이고 음수일 땐 1 로 표기됩니다.

필드 계산값 저장값 (RAM)
부호 (1 bit) + 0
지수부 (8 bit) 3 10000010
가수부 (23 bit) 1.01101 01101000000000000000000

잠깐! 여기서 읽는 것을 멈추시고, 표를 다시 한번 확인해보시기 바랍니다. 제가 숫자를 제대로 기입했나요?

저장된 가수부를 자세히 보면, 앞자리의 1 이 사라진 것을 알 수 있습니다.
지수부는 어떤식으로 계산이 된건지 모르겠지만, 10000010 라는 값으로 저장되어 있습니다.


컴퓨터는 왜 이렇게 저장했을까요?


지수부의 Bias


지수부는 소수점을 얼만큼 이동시킬지 결정하는 부분이라고 했습니다.
소수점을 오른쪽으로 이동하면 양의 지수, 왼쪽으로 이동하면 음의 지수가 필요합니다.

8 bit 로 표현할 수 있는 범위는 0 ~ 255 입니다.
그런데 음의 지수도 표현해야 하는데, 이 범위에는 음수가 없습니다.

그래서 127 을 기준점으로 정하고, 아래와 같이 나누어 사용합니다.

저장값 의미
0 특수값
1 ~ 126 음의 지수
127 0승
128 ~ 254 양의 지수
255 특수값 (∞, NaN)

0255 는 특수값으로, 지금은 몰라도 됩니다.


핵심은, 실제 지수에 127을 더한 값 이 지수부에 저장된다는 것입니다. 이 기준값 127Bias 라고 부릅니다.

지수부 저장값=실제 지수+127\text{지수부 저장값} = \text{실제 지수} + 127

예시를 보면 더 쉽게 이해가 됩니다.

7+127=12001111000(2)0+127=12701111111(2)3+127=13010000010(2)\begin{aligned} -7 + 127 &= 120 \quad \Rightarrow \quad 01111000_{(2)} \\ \phantom{-}0 + 127 &= 127 \quad \Rightarrow \quad 01111111_{(2)} \\ \phantom{-}3 + 127 &= 130 \quad \Rightarrow \quad 10000010_{(2)} \end{aligned}

아까 예시에서 지수부에 10000010 이 저장된 이유가 바로 이것입니다.
3 + 127 = 130, 그리고 130 을 2진법으로 변환하면 10000010 이 됩니다.


가수부의 Hidden Bit / Implicit Bit


이번엔 가수부가 1.01101 이 아닌 01101 로 저장된 이유를 알아보겠습니다.

정규화 과정에서 한 가지 패턴이 있습니다. 정규화된 이진수는 항상 1. 로 시작 한다는 것입니다.

11.25    1011.01    1.01101×230.40625    0.01101    1.101×22\begin{aligned} 11.25 &\;\rightarrow\; 1011.01 \;\rightarrow\; \mathbf{1}.01101 \times 2^{3} \\ 0.40625 &\;\rightarrow\; 0.01101 \;\rightarrow\; \mathbf{1}.101 \times 2^{-2} \end{aligned}

맨 앞의 1 은 어떤 수를 정규화해도 반드시 존재합니다. 그렇다면, 굳이 저장할 필요가 없지 않을까요?
그래서 이 1. 부분은 있다고 약속 하고, 실제 메모리에는 소수점 이하만 저장합니다.
CPU 가 값을 읽을 때 자동으로 맨 앞에 1. 을 붙여 계산합니다.


이렇게 숨겨진 1Hidden Bit 또는 Implicit Bit 라고 합니다.
덕분에 가수부는 23 bit 를 가지지만, 숨겨진 1 을 포함하면 실제로는 24 bit 의 정밀도 를 가집니다.
공짜로 1 bit 를 얻은 셈입니다.


계산


그렇다면 저희가 궁금한 0.1 + 1.1 은 실제로 어떻게 계산될까요?


10진법0.12진법 으로 변환하면, 아래와 같이 무한히 반복되는 순환소수가 됩니다.

0.1(10)=0.000110011001100110011(2)0.1_{(10)} = 0.000110011001100110011\ldots_{(2)}

float 의 가수부는 24 bit (Hidden Bit 포함) 이므로, 24번째 자리에서 반올림하여 저장됩니다.

0.1    1.10011001100110011001101(2)×240.1 \;\approx\; 1.10011001100110011001101_{(2)} \times 2^{-4}

이를 다시 10진법으로 변환하면 아래와 같이 됩니다.

0.1    0.100000001490116120.1 \;\approx\; 0.10000000149011612

같은 방식으로 1.1 도 정확히 표현할 수 없습니다.

1.1    1.100000023841857911.1 \;\approx\; 1.10000002384185791

이 둘을 더하면 아래와 같이 됩니다.

0.10000000149  +  1.10000002384  =  1.200000025330.10000000149\ldots \;+\; 1.10000002384\ldots \;=\; 1.20000002533\ldots

그런데 생각해보면, 수직선 위의 숫자는 무한히 존재하고, float 는 그 모든 값을 저장할 수 없습니다. 그래서 float 는 내부적으로 특정 값들만 미리 정해두고, 점처럼 띄엄띄엄 사용합니다.


계산 결과인 1.20000002533float 가 정해둔 점들 중 하나가 아닐 수 있습니다.
예를 들어, C#float 에선 1.20000004768 을 그 특정 값 중 하나로 두고 있다고 가정해봅시다.
그럴 경우 컴퓨터는 1.20000002533 에 가장 가까운 점인 1.20000004768 을 찾아서 저장합니다.

이 과정을 Rounding 이라고 합니다.

1.20000002533  Rounding  1.200000047681.20000002533\ldots \;\xrightarrow{\text{Rounding}}\; 1.20000004768\ldots

그리고 1.2ffloat 에 저장할 때도 동일하게 반올림이 일어납니다.

1.2(10)  float 저장  1.200000047681.2_{(10)} \;\xrightarrow{\text{float 저장}}\; 1.20000004768\ldots

결국 두 값은 완전히 동일합니다.

0.1f+1.1f1.20000004768  ==  1.2f1.20000004768True\underbrace{0.1\text{f} + 1.1\text{f}}_{1.20000004768\ldots} \;==\; \underbrace{1.2\text{f}}_{1.20000004768\ldots} \quad \Rightarrow \quad \texttt{True}

우연의 일치?


앞서 살펴봤듯이, 0.1f + 1.1f 의 Rounding 결과는 1.20000004768 입니다.
그런데 공교롭게도, 1.2ffloat 에 저장할 때 사용하는 값도 1.20000004768 입니다.


정리하면 아래와 같습니다.

  1. 0.1f + 1.1f 의 실제 계산 결과는 1.20000002533 입니다.
  2. 이 값이 Rounding 되어 가장 가까운 float 점인 1.20000004768 이 됩니다.
  3. 1.2f 의 실제 저장값도 1.20000004768 입니다.
  4. 결국 c == 1.2f1.20000004768 == 1.20000004768 이므로 True 입니다.
float a = 0.1f;
float b = 1.1f;
float c = a + b;

// c     : 1.20000004768… (1.20000002533… 이 Rounding 됨)
// 1.2f  : 1.20000004768… (float 에 저장된 가장 가까운 점)
Debug.Log(c == 1.2f); // True

그렇다면, 이번 문단의 제목을 다시 읽어보겠습니다.
이건 정말 우연의 일치 일까요?


IEEE 754


IEEE (Institute of Electrical and Electronics Engineers)
IEEE 부동소수점 연산 표준(IEEE Standard for Floating-Point Arithmetic, IEEE 754)

지금까지 살펴본 float 의 동작방식은 버그나 우연이 아닙니다.
저희가 살펴본 이것들은 전부 IEEE 754 라는 국제 표준에 정의된 의도적이고 논리적인 동작입니다.

IEEE 754 는 1985 년에 제정된 부동소수점 연산 표준입니다.

  • 32 bit 를 어떻게 나눌 것인지
  • Bias 의 값을 얼마로 할 것인지
  • 반올림은 어떤 방식으로 할 것인지 (여러가지 방법이 있지만, 머리가 아프니 생략하겠습니다.)
  • Hidden bit 를 사용할 것인지

전세계의 99% 가 이 국제표준을 사용하고 있습니다. 이 표준의 목적은 어떤 언어에서든 동일한 부동소수점 연산에 대해 동일한 결과를 보장하는 것입니다. 그렇다면 C#, Python, C++, JavaScript 등의 언어들에서 float 연산을 하면 동일한 결과가 나오고, 0.1f + 1.1f == 1.2f 는 어느 언어에서든 True 일까요?

안타깝게도 그렇지 않습니다. 표준을 따른다고 해서 모든 환경에서 동일한 결과가 나오는 건 아닙니다. JavaScript 는 모든 숫자를 double (64 bit) 로 처리하기 때문에, float (32 bit) 를 사용하는 C# 과는 정밀도 자체가 다릅니다. 또한 컴파일러 최적화, CPU 아키텍처, 런타임 환경 등에 따라서도 결과가 달라질 수 있습니다.

stackoverflow 에 따르면, 변수에 저장하느냐 안하느냐에 따라서도 결과가 달라질 수 있다고 합니다.
직접 계산식을 비교하면 False 지만, 변수에 저장한 후 비교하면 True 가 나오는 경우가 있다는 것입니다.


결론


여러분께 다시 물어보겠습니다.
0.1 + 1.1 == 1.2False확실한가요?


제가 대신 대답을 해보자면, “일반적으로 False 가 맞다” 라고 말할 것 같습니다.
이 글에서 말하고자 하는 것은, 무조건 False 라고 대답하는 것이 항상 맞는 논리는 아니라는 점입니다.
사람들과 책이 알려주는 것을 한번쯤 의심해보는 자세도 나쁘지 않다고 생각합니다.


결론은, 소수를 쓸 땐 조건문을 조금 더 신경쓰고, 특히 == 연산은 쓰지 않는 것이 좋겠습니다.

Result Result is false, But this may be a bias.
Bias 는 컴퓨터에선 지수를 양수로 만드는걸 의미하지만, 일반적인 의미는 편견, 편향이라는 뜻.


Reference


© Powered by Minssuy