(Swift) Float 타입 사용법

Float 타입은 32비트 부동 소수점 숫자를 다루기 위해 제공되는 기본 타입이다. 64비트 정밀도를 가지는 Double 타입도 제공된다.


지수(exponent)와 가수(significand)에 대해

컴퓨터에서 사용하는 부동소수점 숫자는 기본적으로 근사값이다. 모두들 알다시피 컴퓨터는 내부적으로 모든 데이터를 이진수로 표현한다. 정수나 자연수에 있어서 진법은 특정한 값을 표시하는데 있어서 필요한 숫자의 개수만 달라지지만 소수점 이하의 값에 대해서는 그 사정이 다르다. 먼저 10진수 13을 생각해보자. 13은 10 + 3 이며 이를 10을 밑으로 하는 다항식으로 쓰면 1 \times 10^1 + 3 \times 10^0 의 꼴로 표현된다. 같은 식으로 이 값을 이진수로 표현하면 1011(2)가 되는데, 이 표현은 다시 이는 1\times2^3 + 0\times2^2 + 1\times2^1 + 1\times2^0 이라는 의미이다.

그렇다면 이진수 0.1(2)은 얼마일까? 십진수 0.1은 1/10 이며 이는 곧 1\times10^-1 이다. 따라서 이진수 0.1(2)은 같은 식으로 1 \times 2^{-1} = frac{1}{2} = 0.5 으로 표현된다. 자 이제 0.1 을 2진수로 표현하려면 어떻게 계산해야 할까? 이 문제는 2의 거듭제곱을 분모로하고 분자가 1인 분수들의 합이 1/10이 되는 수들을 찾아야 한다는 의미이다. 깊게 계산하는 과정을 생략하고 그 결과만 봤을 때, 이 때 필요한 분수의 개수는 무한히 많다. 즉, 0.1을 2진수로 표현하면 무한 소수가 된다.

고작 0.1인데, 이걸 2진수로 표현하면 무한소수가 된다는 사실이 당혹스럽게 느껴질 수도 있는데, 1/3 만 해도 무한소수로 표현되지만 3진법에서는 그냥 0.1로 표현 가능한 점을 생각해보면 그리 억울할 일도 아니다.

대략 소수점 11자리까지의 정밀도로 표현하면 십진수 0.1은 이진수로 0.0001100110011(2) 쯤 되고, 이 값을 다시 십진수로 변화하면 0.0999755859375 정도가 된다. 즉 엄밀하게 0.1이 아닌 것이다. 2진법으로 표현했을 때 딱 떨어지는 값이 아닌 이상, 많은 실수값들이 컴퓨터에서는 실질적으로 근사값을 사용하고 있는 셈이다.

수학에서 근사값을 표현하는 방식으로 가수부와 지수부를 나눠서 표시하는 방식이 있다. 예를 들어 12500 이라는 값이 있을 때, 이를 1.25 \times 10^4 으로 표현하는 것이다. 이 때 1.25를 가수부라 하고 10^4을 지수부라 한다. 이 표현을 사용하면 12500은 1.25 \times 10^{4} 으로 나타낼 수도 있고, 1.250 \times 10^4 으로 나타낼 수도 있다. 둘 다 같은 값 처럼 보이지만, 후자의 표현은 10의 자리 숫자 0까지 신뢰할 수 있는 유효숫자라는 것을 알려주는 표기방법이다.

많은 프로그래밍 언어에서 실수를 표현하는 타입들은 이 표현을 데이터를 저장하는 구조에 그대로 반영한다. 즉 부호, 가수부, 실수부로 값이 이루어진다고 보고, 그것들을 메모리에 잘 정돈해 넣어두는 것이다. Swift의 Float 역시 이렇게 가수부와 실수부를 나눠서 저장하고 있으며, 프로퍼티를 통해서 각각의 값을 알 수 있다. 가수 값은 significand 로 지수 값은 exponent로 구할 수 있다. 이 때, Float 값이 사용하는 지수의 밑은 (당연하게도) 2인데, 이는 Float.radix로 정의되어 있다. 즉 실수 값은 다음과 같이 정의된다. (아래에서 ** 은 거듭제곱을 의미하는 표현이며, Swift의 정식 연산자는 아니다.)

let magitude = x.significand * F.radix ** x.exponent

예를 들어 0.1 을 Float으로 표현하면, 가수부는 1.6 이고 지수는 -4이다. 따라서 1.6 * 2 ** -4 를 계산하여 0.1을 표현하게 된다.

용어 – magnitude

magnitude는 말 그대로 ‘크기’라는 의미이며, 수직선 상에서 원점으로부터 얼마나 멀리 있는가를 의미한다. Float 타입은 magnitude 속성을 가지고 있어서 이로부터 그 값을 알 수 있다. 하지만 이 의미는 실질적으로 ‘절대값’을 의미하는 경우가 많기 때문에, 표준함수 abs(_:)를 사용할 것이 권장된다.

제곱근

초기 Swift에는 제곱근을 구하는 단항 연산자나 별도의 수학함수가 존재하지 않았다. 초기 언어의 표준 라이브러리가 좀 부실할 수도 있을 것이고, 그 시점에서 이미 Swift는 C 라이브러리를 얼마든지 가져다 쓸 수 있었기때문에 C의 함수들을 사용하는 방법이 이미 존재했었다. 당시 Swift에서 제곱근을 구하기 위해서는 C에 정의된 sqrt() 함수를 사용하는 방법이 있었다.

문제는 C의 sqrt함수의 원형은 double sqrt(double f); 이기 때문에 이 함수는 Swift로는 (Double) -> Double 로 반입된다는 점이다. C에서는 float을 double로 혹은 그 역방향으로 암묵적으로 캐스팅하는 것이 가능했고, 많은 경우에 별 문제가 되지 않았다. 비슷하게 Swift에서도 명시적으로만 해주기만 하면 큰 문제는 없는데, 이게 엄청 귀찮고 불편하다는 것이다.

let a: Float = 3.7
let b: Float = Float(sqrt(Double(a)))

Xcode 9.0부터(Swift 4.2) Float/Double 타입에 몇 가지 프로퍼티 및 메소드가 추가되었는데, 여기에 squareRoot()가 포함된다. 즉 Float은 이제 그 스스로가 제곱근을 구하는 메소드를 제공하게 된 셈이다. squareRoot()외에 formSqaureRoot() 메소드가 있는데, 이는 자기 자신의 값을 제곱근으로 변경하는 형태로 사용된다.

나눗셈의 나머지 계산

실수의 나눗셈에서 나머지를 계산하는 것은 흔한 일은 아니지만 일단 한 번 알아보자. 사실 나머지는 정의하기에 따라서 여러 단위로 나올 수 있다. 보통은 나눗셈에서 제수와 피제수의 관계식을 사용하면서 나머지를 정의한다.

y = x * q + r

보통의 나눗셈 등식을 이용할 때, 제수, 피제수는 양의 정수였고, 따라서 몫과 나머지 역시 양의 정수였다. 따라서 보통 r의 범위는 0 이상, x 미만의 정수에 오게 된다. 실수의 나눗셈에서 몫과 나머지는 조금 다르게 정의된다. 먼저 몫은 y \div x에 가장 가까운 정수가 된다. 예를 들어 2.3이라면 2가 될 것이고, 2.7이라면 3이 될 것이다. 그렇다고해서 우리가 알고 있는 반올림과는 약간 다르다. 반올림에서는 소수점 아래가 0.5인 경우 더 큰 쪽으로 값을 정했다. 하지만 Float의 나눗셈에서 몫은 0.5가 되는 경우, “짝수인 정수”가 된다. 따라서 2.5의 경우에는 2가 되며, 3.5의 경우에는 4가 된다. 만약 부호가 다른 두 실수에 대한 나눗셈이라면 이 정의에 따르면 나머지가 음수일 수도 있게 되는 셈이다.

Float 값 끼리 나눈 나머지를 구하는 것은 remainder(dividingBy:) 메소드로 구할 수 있다. 그리고 나머지 값을 항상 0보다 큰 수로 맞추고 싶다면 (즉 몫을 조정해서 나머지를 0..<x의 범위로 한정하고 싶다면) truncatingRemaider(dividingBy:)를 사용한다.

예를 들어 큰 이미지를 정수개의 타일로 나눠야 하는 경우에, 가로/세로의 크기가 타일 크기의 정수배가 아니라면 잘리는 값이 나오게 된다. 이 때 경계에 속하는 영역의 크기를 구하고 싶다면 항상 양이 되는 나머지를 찾아야 하는데, 이런 경우에 후자의 메소드를 사용한다.

let a: Float = 8.625
let b: Float = 0.75
let c = a.remainder(dividingBy: b)
// a / b = 11.5 ... -0.375
// 양의 나머지를 구하고 싶다면
let d = a.truncatingRemainder(dividingBy: b)
// => 0.375

반올림

.rounded() 메소드가 반올림된 값을 리턴한다. mutating 버전의 .round()도 사용할 수 있다. 이 두 메소드는 각각 FloatingPointRoundingRule 타입의 값을 인자로 받는 별개의 버전이 존재한다. 왜냐하면 반올림에도 규칙이 있을 수 있기 때문이다.

  • .awayFromZero – 0에서 멀어지는 쪽으로 간다.
  • .toNearestOrAwayFromZero – 가까운 쪽을 선택하며, .5 에 머무는 경우에는 0에서 먼쪽을 선택한다. 디폴트 동작이며, 흔히 학교에서 배우는 반올림 규칙이 이것이다.
  • .down, .up – 각각 더 작은 값, 큰 값을 향한다.
  • .towardZero – 내림과 같은 동작
  • .toNearestOrEven – 가까운 쪽으로 .5인 경우 짝수가 되는쪽을 선택한다.

즉 어떤 룰을 사용할지를 활용하면 floor/ceil을 대체하는 용도로 쓸 수 있다.

랜덤값

static 메소드인 random(in:) 은 특정한 범위 내에서 랜덤한 실수 값을 골라준다. 이 때, 연속적인 균일 분포(continuous uniform distribution)를 사용하며, 실제로는 골라진 값에 가장 가까운 Float 표현을 리턴한다. 따라서 반복적으로 사용하는 경우, 일부 값이 다른 값보다 더 자주 나올 수 있다. 만약 랜덤 제너레이터를 별도로 지정하고 싶다면 random(iin: using:) 을 사용한다. 이 때 using: 에는RadomNumberGenerator 프로토콜을 따르는 타입이 오며, 이 파라미터는 inout으로 전달되어야 한다.

그외의 속성들

Float.zero, Float.pi, Float.infinte 이 나름 자주 쓰이는 상수로 정의되어 있다. 또한 isZero, isFinite, isInfinite, isNaN 등의 상태를 알려주는 프로퍼티들을 사용할 수 있다.

Double 타입에도

Double 타입에서도 위에서 언급한 기능들이 동일하게 적용된다.