https://www.youtube.com/watch?v=-GsrYvZoAdA
예전에 이런 글을 본 적이 있다. 0.1 + 1.1 != 1.2 라니
그 때 궁금해져서 영상을 보긴했는데 부동소수점 개념이 잘 이해되지 않아서 그냥 그런가보다하고 넘어갔다.
이번에 자바의 정석을 읽으며 부동소수점에 대한 내용이 나와서 한 번 제대로 조사해보려한다.
오늘 내용은 자바의 정석과 함께 몇 가지 블로그들을 뒤져가며 알게된 내용을 기술한다
따라서 자바의 정석과는 100% 내용이 같지 않을 수 있으며, 추가적인 내용들이 들어가있을 수 있다.
들어가기 앞서..

자바의정석 독서 #1에서 이 내용이 나오는데, 당당하게 틀렸다. 부동소수점 방식에서는 어느 부분에서 소수가 시작되는지 정해두지 않았다. 그래서 부동소수점이기에.. 이 내용을 지금부터 교정하려한다.
여기서는
- 실수의 오버플로우 / 언더플로우
- 부동소수점 표기방식
- 정수를 어떻게 부동소수점으로 변환할까?
로 알아보도록 하겠다.
실수형 변수에서도 오버플로우가 있고, 특이한 것은 언더플로우도 있다는 것이다. 실수형 변수에서 오버플로우가 일어나게 되면 값이 무한대로 발산하며, 실수의 값이 너무 작아진다면 변수의 값이 0으로 변한다.


이렇게 double 또는 float가 저장할 수 있는 값을 벗어나면 정수형 변수처럼 엉뚱한 값이 나오는게 아니라 Infinity (무한대) 또는 0이 출력된다.
그렇기 때문에 double로 예시를 들면 -4.9*10^-324 구간에서 4.9*10^-324 구간은 표현하지 못한다. float는 더 이 범위가 크고
다만 4바이트 방식에서 int보다 float가 저장할 수 있는 값이 더 많은데, (int는 2*31 - 1 개까지 표현할 수 있지만 float는 3.4 * 10^38 까지 표현할 수 있다) 그 이유는 값 저장방식의 차이 때문이다. int는 4바이트가 모조리 값을 표현하는데 쓰인다.

반면 float와 double은 부호 / 지수 / 가수 부분으로 이루어져 있다.
따라서 값을 표현할 때 (부호)(가수) * 2^(지수) 형태로 표현한다. 이 방식을 부동소수점 방식이라고 칭한다.
부호는 정수형과 동일하게 작동하는 1비트 데이터다. 정수형과 달리 2의 보수법을 사용하지 않아서 그냥 양수면 0, 음수면 1로 처리한다.
지수는 부호가 있는 정수이며, 부동소수점 표기에서 2의 지수로 사용된다. float에서 8비트, double에서 11비트 크기의 공간을 가진다. 다만 이 때 지수의 최솟값과 최댓값은 양의무한대, 음의무한대 등으로 예약되어 있기에 사용할 수 없다. 따라서 float로 나타낼 수 있는 최댓값은 2^127이며, (지수 최댓값 128, 예약된 숫자 제외) 이는 약 10^38이다.
가수에는 실제 값이 저장된다. 상용로그에서 log20 = 1 + log2로 표현할 때, 1이 가수다. float에서 23비트, double에서 52비트를 가진다.
이렇게 부동소수점은 ± M*2^E 형식으로 실수를 표기하는 방식이다. (M : 가수(Mantissa), E : 지수(Exponent))
그러면 자연수를 부동소수점으로 바꿔보도록할 때는 어떨까
소수 328.625가 있다고 하자.
이를 이진수로 변환하면 101001000.101 이고, 이를 1.xxx * 2^n 형태로 변환하는 정규화 과정을 거치는데, 이를 변환하면
101001000.101 → 1.01001000101 * 2^8 이다. 정규화된 이진 실수는 모두 1... 형태를 띄므로 소수점 뒷자리가 가수로 지정된다.
즉, 여기서 이 가수는 float에서 23개(23비트), double에서 52개(52비트)를 저장할 수 있다. 여기서 가수는 01001000101. 11자리이므로 float로도 적당하다는 것을 알 수 있다.
그 다음 지수는 8인데, 여기서 기저(bias) 127을 더한 135를 이진수로 변환하게 된다. 이는 10000111이다.
왜 기저를 더하냐면 이는 지수를 표현하는 방법에서 기인하는데 지수는 첫비트가 1, 0으로 음수를 구분하는 것이 아니라 초과표기법을 통해서 양수와 음수를 구분하기 때문이다.
초과표기법은 단순하게 이진수 000을 최소값으로 설정하고 계산하는 방법이다. float의 경우에는 지수에 8비트가 부여되므로 2^7 을 기저로 하면 전체 2^8 개중에서 절반은 음수로, 절반은 양수로 삼는다. 그래서 기저를 더해서 음수까지 양수로 표현한다. 나중에 실수로 변환할 때는 십진수로 바꿔서 기저를 빼는 방식으로..
예를 들어보자면 float는 지수에 8비트가 부여되므로
-127
-126
.
.
.
0
1
.
.
.
126
127
128
이렇게 지수는 총 2^8개의 값을 사용할 수 있어야한다. 하지만 초과표기법에서는 부호용도의 비트가 따로 없으므로 127을 더해서
0
1
.
.
.
127
128
.
.
.
253
254
255
이런식으로 값을 저장하고, 나중에 십진수로 바꿀 때만 127을 빼 음수로 변환하는 방법이다.
따라서 기저는 지수에 할당되는 비트 수에 따라 다른데, 지수에 할당되는 비트 수를 n이라고하면 2^(n-1) - 1 (또는 2^(n-1) 이다.
왜 이런 방식을 쓰냐면.. 이러면 편하다. 특히 이러한 방식은 메모리에 직접적으로 값이 저장할 때 쓰이는데 2의 보수법이니 이런걸 쓰면 회로나 로직이 복잡해지기 때문
암튼 328.625 = 1.01001000101 * 2^8 까지 구했고, 기저까지 더해서 지수부분은 십진수 135, 이진수 10000111이다. 부호는 양수이므로 0이다. 따라서 328.625를 저장하면 저장되는 값은
01000011101001000101000000000000 (0 / 10000111 / 01001000101000000000000)이다. 와우 (가수 부분에서 비트가 남으면 0으로 채운다.)
float는 가수가 23자리의 이진수로 표현할 수 있는데, 이는 약 7자리의 십진수를 표현할 수 있다. (이걸 따로 계산할 수 있는데 굳이 귀찮아서 안함) 따라서 float를 출력하면 소수점 아래 7자리까지만 표현되고, double도 그만큼의 제한이 있는 것이다.
다만 7자리의 정확도를 표현한다와 소수점 아래 6자리를 표현한다는 같은 표현인데, 어차피 7번째 자리에서 반올림해서 6번째 자리에 넣어주기 때문이다.
마지막으로, 다시 0.1 + 0.2 != 0.3 으로 돌아가보자면,
십진수를 이진수로 바꾸는 예시를 들었던 328.625에서 소수부분 0.625는 이진수로 표현하면 0.101로 짧게 표현이 가능하지만, 세상에는 무한소수도 있고 끝없는 이진수가 나열되는 십진소수도 있고, 끝없이 나열되지는 않지만 가수부분이 23비트 (때로는 52비트)를 넘어가는 십진소수도 있다.
따라서 이런 수를 저장하게 되면 가수부분에서 값을 전부 저장하지 못하고 어느순간 그 아래의 값을 버려야할 때가 있는데, 이렇게 버리게 되면서 오류가 나기 시작한다. 이진수니까 올림이 자주 발생하고 결국 흘러흘러 0.1 + 0.2 != 0.3이 되는거
따라서 저장할 수 있는 값이 적은 float보다는 double을 사용하는 것이 좋고, 자바의 소수점 표준을 double로 저장한 것도 이 이유에서다. 슈퍼 마리오 개발하는 것처럼 메모리를 아껴야하고 시간도 줄여야한다면 float를 쓰는게 맞지만, 요즘 컴퓨터들은 그거 아낀다고 큰일이 생기지는 않기 때문에 double을 주로 쓰자.
자바 뿐 아니라 C에서도 공통된 영역이다. C프 공부를 이렇게 할 줄이야...!
위 수식이 틀린 이유는?
컴퓨터에서 소수는 부동소수점으로 값을 저장하는데, 0.1과 0.2 등 십진소수는 대부분 이진소수로 변환하는 과정에서 무한소수가 된다. 이 무한소수를 저장하는 과정에서 메모리의 크기를 벗어난 값은 버려진다. 이 과정에서 생겨난 오차가 연산과정에서 누적되어 결과적으로는 오차가 생기게된다. (GPT에게 한 번 물어보고 교정본)
컴퓨터에서 소수는 부동소수점으로 값을 저장하는데, 0.1과 0.2 등 십진소수를 이진소수로 변환하고 저장하는 과정에서 주어진 수가 수를 저장하기 위해 메모리에 배정된 데이터의 크기를 벗어나게되면 벗어난 부분은 버리고 메모리 크기에 맞게 수가 저장된다. 따라서 이 과정에서 값의 오차가 생기게되며, 오차가 생긴 두 수를 더하게 되어 다른 값이 나오게 된다. (원본)
설명하는 것은 좀 어렵다.. 부동소수점 계산방식까지 꺼낼 수도 없고.. 암튼 늘 궁금하던 부동소수점 저장방식도 배우고 괜찮은 배움이었다.
3줄 요약
1. 부동소수점은 ± (가수) * 2^(지수) 형태로 표현되며, (부호)(가수)(지수) 순서대로 값이 저장된다.
2. float와 double은 저장할 수 있는 가수와 지수값의 범위가 다르며, double이 더 범위가 크다.
3. 십진소수가 이진수로 변환했을 때 메모리의 범위를 넘어서게되면 메모리를 벗어난 부분은 잘라내고 계산하는데, 이 과정에서 오차가 생긴다.
'언어공부 > Java | Kotlin' 카테고리의 다른 글
| [CS] 자바의 정석 독서 #5 - 연산자 (0) | 2025.09.22 |
|---|---|
| [CS] 자바의 정석 독서 #4 - 형변환 (0) | 2025.09.21 |
| 인프런 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 후기 (0) | 2025.09.20 |
| [CS] 자바의정석 독서 #2 - 포매팅 (0) | 2025.09.18 |
| [CS] 자바의 정석 독서 #1 - 2장 - 변수 (0) | 2025.09.18 |
