프로그래밍 언어에서 숫자란 무엇인가?
컴퓨터 계열 전공자라고 해도 아날로그와 디지털의 의미를 명확히 아는 사람은 드물 것이다. 구분은 하더라도 말이다. 컴퓨터에서 다루는 숫자는 유한 자릿수이다. 유한 자릿수란, 최댓값과 최솟값, 그리고 유효 자릿수(정밀도)가 정해져 있어 불연속적인 값만 사용할 수 있다. 불연속적이란 '띄엄띄엄'이라는 의미로 컴퓨터는 연속적이지 않은 값을 처리하는 기계이다(유키오, 2017).
digital [형용사] 손가락(모양)의, 숫자를 사용하는, 디지털 방식의;
analog [형용사] 아날로그(데이터를 연속적으로 변화하는 양으로 표현하는 방식)의, 아날로그 표시의;
영화 <매트릭스>라는 컴퓨터 안의 가상세계에서는 현실과 달리 0과 1로만 이루어진 띄엄띄엄한 세상이다. 영화의 가상공간에서 오히려 현실에서 보는 자연계보다 생생한 느낌을 가질 수 있고, 실제로 먹은 게 없더라고 가상세계에서의 득템은 포만감을 더 일으키게 할 수 있더라도 디지털은 물리적인 현상을 유한 자릿수의 숫자로 변환한 후 처리되어 보이는 결과물의 하나일 뿐이다.
컴퓨터 내부는 모든 것을 숫자로 표현하고 숫자로 처리하기 때문에 컴퓨터 언어에서 숫자로 표현하는 방법에 대해 이해하는 것이 중요하다. 실제로 디지털로는 현실 세계를 완전히 표현할 수가 없고 매트릭스는 영화에서나 표현 가능한 인공지능에 불과하지만 디지털에는 아날로그보다 편리한 점이 많이 있다. 그중 하나가 컴퓨터에서 다루기 쉽다는 점이고, 또 다른 하나가 복사를 해도 손상이 되지 않는다는 점이다. 이러한 특징을 살리기 위해 컴퓨터에서는 모든 현상을 디지털(0과 1의 2진수)로 표현한다(유키오, 2017).
프로그램을 작성할 때, 16진수나 8진수 혹은 10진수 등의 숫자 등으로 작성하더도 컴퓨터의 내부에서는 동일한 값으로 취급된다. 즉, 10진수 및 16진수로 작성했을 때, 해당 진수로 처리하는 것이 아니라 2진수로 처리된다. 그 이유는 앞에서도 설명했지만, 컴퓨터의 내부는 디지털(0과 1의 2진수)로 처리하는 공간이기 때문이다. 2진수는 인간이 사용하기에서 번거로운 숫자이기 때문에, 프로그램 작성 시에는 일상생활에 익숙한 10진수나 2진수와의 대응 관계가 알기 쉬운 16진수를 사용한다(유키오, 2017).
위의 프로그램 소스는 두 개의 주사위를 동시에 던졌을 때, 둘 다 1이 나오려면(뱀의 눈 모양과 비슷하다고 하여 영어로 Snake Eyes라고 표현한다.) 평균적으로 얼마나 주사위를 굴려야 하나의 문제를 코딩하였다. 1,000번을 던지는 시뮬레이션을 돌렸을 때, 결괏값으로 몇 번째인 경우에 둘 다 1이 나왔는지와 그 값들의 합을 1,000번의 던진 횟수로 나눈 평균값을 출력시켰다. 여기서 사용한 내장형 함수인 Math의 random() 메써드의 기능과 그 계산식의 의미는 아래와 같다.
The subroutine (actually this is calling the class Math and the method random) Math.random() will return a number that is between 0 and 1. This number is a floating point number. By multiplying it by the number 6 we will get a random number between 0 and 5. This number will be a floating point number and is likely to be a fraction meaning that it will have digits to the right of the decimal point.
자연계에서 실수(Real number)로 계산할 경우, 평균값을 구할 때는 숫자를 나눠야 하므로 소수점이 발생할 수 있다. 컴퓨터 언어에서 숫자는 정해진 범위 안에서 표현되고, 그 범위 안에서 처리된다. 이 정해진 범위를 데이터 형(Date type)이라고 부른다. 컴퓨터에서도 이 데이터 형을 크게 정수형과 실수형(부동 소수점형)으로 나뉜다. 컴퓨터가 위의 프로그램의 평균값을 구하려면 데이터 형에 따라 허용할 수 있는 범위 내에서 처리해야 한다. 정수형의 기본적(부호가 없을 때, Unsigned Integer)으로 int형이며, 2 bytes(16 bits)므로 2의 16승만큼(0~65535)의 숫자로 표현할 수 있다.
부동 소수점형의 기본형인 double은 유효자리가 대략 14자리 정도이다. 컴퓨터에서 소수점을 표현할 수 있으면 모든 숫자를 표현할 수 있을 것 같지만, 앞서 말한 '유횻값'이란 것이 있기 때문에 모든 숫자를 표현할 수는 없다. 이는 부동소수점(double)에서 가수부에 해당하는 비트수로 결정된다. 부동소수점이란 물리나 화학에서는 아보가드로 수(Avogadro's number)라고 하여 수치를 표현하는 부분과 자릿수를 표현하는 부분을 나눠서 표현하는 방법이다. 이를테면 한 번씩 봤을 직한 6.02 X 10의 23승(6.02E + 32)이나 0.1 X 10의 마이너스 16승(1E - 16)과 같다(유키오, 2017).
컴퓨터의 기본은 처리 속도가 빠른 정수형이지만 소수를 다룰 수 있는 실수(부동소수점)형도 평균값 등의 수치를 구할 때 정밀한 계산을 위해서 사용한다. 위의 자바 소스처럼 float형으로 업캐스팅(casting: The cast is a way to force java to convert data from one data type to another. 이 경우는 기존에 int형으로 초기화한 SIZE 상수를 float형으로 상위 데이터 범위로 전환시켜서 나누기하였고, 그 나눈 값도 float형으로 선언하였다.)할 수 있다. 문제가 생기지 않는 정도까지만 숫자의 정밀도를 높이면 된다. 위의 결괏값은 17.424인데, 확률에 대한 평균값으로 소수점 이하 1자리까지만 있으면 충분하다. 17.42이나 17.424 등을 17.4로 간주하면 딱히 수학자가 아닌 이상 아무 문제가 없기 때문이다. 이 말은 즉, 17.4와 17.5 사이의 숫자를 고려하지 않아도 문제가 없다는 말이다.
이처럼 연속적이지 않은 값이어도 상관없을 때 특히 컴퓨터의 능력이 발휘된다.
역으로 아날로그의 자연세계가 더 과학적이며 과학으로도 설명할 수 없는 현상계라고 말할 수 있다. 컴퓨터가 처리할 수 있는 숫자의 자릿수에는 한계가 있기 때문이다. 만약 50의 팩토리얼(50!)을 구하라면 재귀 호출을 떠나서 결괏값이 65자리이기 때문에 이렇게 자릿수가 많아지면 직접적인 계산으로는 값을 구할 수가 없다. 다시 말하지만 컴퓨터 언어에서 숫자는 정해진 범위 안에서 표현되고, 그 범위 안에서 처리된다라는 점을 명심해야 한다. 이 정해진 범위를 무엇?
컴퓨터에서 가수부의 비트 수로 유횻값이 결정되는 부동소수점은 모든 숫자를 표현할 수는 없고(특히 컴퓨터는 순환소수와 소수-자기 자신과 1만을 약수로 가지는 수의 표현이 힘들다), 처리하는 컴퓨터에 따라 다르지만 float형은 유효 자리가 대략 7자리(여기 프로그램에서 소숫점 3자리까지 출력됨.)이고, double은 대략 14자리 정도이다. 이 부동소수점을 사용할 때는 조심해야 할 점이 있다. 가장 주의해야 할 것이 오차이다. 부동소수점의 계산에는 오차가 있기 마련이다. 예컨대, 0.1을 100번 더해도 정확하게 10으로 딱 떨어지지 않는다. 10진수의 0.1을 2진수로 표현하려고 하면 무한대의 자릿수가 필요하기 때문이다.
"이처럼 컴퓨터가 2진수로 계산한 값과 실제로 10진수로 계산한 결과가 일치하지 않는 경우가 있다. 그렇기 때문에, 두 개의 부동소수점이 동일한지는 비교할 수 없다.
a의 값이 0.1인지 아닌지를 알아보는 것은 어렵지만, a의 값이 0.099999999와 0.1000000001 사이에 있는지를 알아보는 방법으로 근사치에 있는지를 확인하고 그렇다면 같다(=)라고 간주한다.
그리고 앞서 말했듯이 유효 자리에도 주의해야 한다. 예를 들면, 50의 계승(50!)을 표현하기 위해서는 53개의 자리가 필요하다. 그러나, 14자리 정도의 정밀함 밖에 표현할 수 없기 때문에 50의 계승에 가까운 값을 표현할 수 있는 있지만, 완전히 동일한 값을 표현하는 것은 불가능하다." (유키오, 2017)
10진수의 0.1을 2진수로 표현하면 0.0001 ~ 0.0010의 사이에 있다고 생각하면 된다. 이와 같은 방법으로 10진수의 0.1을 2진수로 표현하면 다음 그림과 같다.
정말 위의 그림에서처럼 0과 1의 무한대의 자릿수가 나열된다. 다만 맨앞에 점(.)을 붙이고 말이다. 즉, 순환소수가 되고 만다. 컴퓨터에는 기억 영역이 한정되어 있기 때문에, 무한대의 자릿수를 표현할 수 없다. 순환하는 소수를 적당한 선에서 중단해야 한다. 도중에 자르면 오차가 발생하겠지만 어쩔 수 없다. 이런 이유에서 0.1을 100번 더해도 10이 되지 않는 것이다.
0.1뿐만이 아니다. 컴퓨터에서는 2진수로 계산하면 다양한 오차가 발생한다. 미묘한 차이의 숫자의 경우, 버림 및 올림, 반올림 등을 할 때 컴퓨터의 기종에 따라 그 결과가 달라지는 경우도 있다.
다음 편에 2진수를 음수로 나타내기 위해 필요한 보수와 그로 인해 나타나는 오버플로에 대해 다루겠다.
참조
- 무라야마 유키오. 이해란 역. (2015). C를 배우기 전에 반드시 알아야 할 것들, 서울: 루비 페이퍼.