데이터베이스/MySQL

[MySQL]DECIMAL vs DOUBLE(FLOAT): 뭘 선택해야 할까?(Feat. 고정 소수점 vs 부동 소수점)

Woonys 2024. 3. 25. 01:33
반응형

TL;DR: 핀테크라면 닥치고 DECIMAL이다 명심해라

 

Introduction

지난 달까지 넥스트스텝에서 진행하는 ATDD(Acceptance Test Driven Development) 강의를 들었다. 지난 TDD 강의와 마찬가지로 이번 강의 역시 아주 훌륭했다. 특히 실무에 적용한다는 관점에서 볼 때 TDD는 단위 테스트 위주라 서비스 레이어에 비즈니스 로직이 올라가있는 레거시 환경에서는 실천하기가 꽤나 어려웠다. 반면 ATDD는 도메인 레이어부터 짜는 방식이 아니었다. 인수 조건을 설정하고 가장 바깥에서부터 Integration test를 작성했다가 다시 가장 안쪽으로 돌아와 도메인 테스트를 작성하는 등, 작성자가 입맛에 맞게 그때그때 편한 방향을 추구하면서 자유롭게 테스트를 짤 수 있어 실무에 적용하기가 훨씬 용이했달까. 덧붙여 인수 조건에 대해 깊이 있게 고민해볼 수 있던 점 역시 아주 좋았다. 개발자 <-> PM <-> QA 사이 소통의 인터페이스라는 관점에서 인수 조건을 다시 볼 수 있었다. 단순히 개발을 잘한다를 넘어 어떻게 하면 제품을 만드는 과정에서 내부 커뮤니케이션을 원활하게 가져갈 수 있을지에 대해 더 많이 배울 수 있던 시간이었다.

 

주제와 전혀 동떨어진 내용인 것 같은데 이렇게 길게 적는 이유는, 해당 포스트를 작성하게 된 계기가 ATDD 오프라인 모임을 가졌을 때 나왔기 때문이다. 주말에 진행했던 ATDD 오프라인 모짝미(모여서 짝 프로그래밍 미션)에서 미션이 끝난 뒤 다른 분들과 담소를 나눌 기회가 있었다. 그러다 각자 회사가 속해있는 도메인을 소개하게 됐는데, 핀테크라고 하니 다른 분께서 이렇게 질문을 주셨다.

 

어, 핀테크면 금융 데이터 저장할 때 DECIMAL 쓰세요, DOUBLE이나 FLOAT 쓰세요?
(DECIMAL 쓴다고 답하니)
근데 그러면 속도 많이 느리지 않아요? 문제 없나요?

 

 

당시에는 회사 규모가 그 정도로 엄청 큰 건 아니라서 괜찮아요 하하 하고 얼버무렸는데 꽤나 충격받았다. 1번은 위 질문에 대해 명료하게 엔지니어링 관점에서 답하지 못했기 때문이며 2번은 "왜 나는 한 번도 이에 대해 고민해본 적이 없던 것인가...?"라는 의문이 튀어나왔기 때문이다. 항상 바깥에 더 배울 게 없는지만 신경썼지, 정작 내부에 우리가 하고 있는 것들에 대해 얼마나 잘 꿰뚫고 있는지에 대해서는 왜 고민하지 않았을까. 반성의 의미로 해당 아티클을 작성해본다.

FLOAT(DOUBLE)과 DECIMAL: 뭐가 다른데?(Feat. 부동 소수점 vs 고정 소수점)

  1. FLOAT & DOUBLE(부동 소수점)

FLOAT(DOUBLE) 타입은 실수를 표현하는데 쓰이며, 이진 부동 소수점 방식으로 숫자를 저장한다. 부동 소수점은 개발자라면 잘 알 텐데, 컴퓨터에서 실수(소수점이 포함된 숫자)를 표현하는 방식 중 하나이다. 이때, 이 방식의 핵심은 소수점의 위치가 고정되지 않는다(부동)는 점이다.

 

부동 소수점 표현 방식은 지수(소수점 위치)와 가수(유효 숫자)를 활용한다. 123.456이라는 숫자가 있을 때, 1.23456 * 10^2와 같이 표현할 수 있다. 이렇게 되면 큰 수나 작은 수를 효율적으로 표현할 수 있을 뿐만 아니라 고정 소수점보다 훨씬 더 넓은 범위의 숫자를 표현할 수 있다.

 

이를 컴퓨터에서는 어떻게 사용할까? 예시로 123.456이라는 숫자를 32비트 이진 부동 소수점으로 표현해보자. 컴퓨터는 이진법을 사용하기 때문에 이를 이진수로 변환하면 0.11110011011001100110011001100110이다. 이때, 맨 앞의 0은 부호를 나타내고(1이면 마이너스) 1을 제외한 소수점 이하 부분을 사용하는데 11110011011001100110011001100110이 가수가 된다. (부동 소수점을 여기서 자세하게 짚고 넘어가진 않겠다..필요하면 다른 글을 찾아보도록 하자)

 

어쨌거나 이 부동 소수점 형식으로 숫자를 저장하게 될 경우 가장 큰 문제는 근사치로 인한 오차이다.부동 소수점 형식의 경우 숫자 값의 길이에 따라 유효 범위의 소수점 자릿수가 바뀐다. 그래서 부동 소수점을 사용하면 정확한 유효 소수점 값을 식별하기 어렵고 그 값을 따져서 크니 작니 비교하는 것 역시 쉽지 않다. 예컨대 십진수를 이진수로 변환하는 과정에서 순환소수가 발생할 수 있다. 부동 소수점 방식은 순환소수가 발생할 경우 지원하는 비트 크기 안에서 꼬리를 잘라버린다. 근삿값을 사용하다보니 정밀한 계산을 할 경우 오차가 발생할 가능성이 크다.

 

MySQL에서 FLOAT는 정밀도를 명시하지 않을 경우 일반적으로 4바이트를 사용해 유효 자릿수를 8개까지 유지한다. 정밀도를 명시하면 최대 8바이트까지 저장 공간을 사용할 수 있다. DOUBLE의 경우 기본적으로 8바이트의 저장 공간을 사용하며 유효 자릿수를 16개까지 늘릴 수 있다.

 

  1. DECIMAL(고정 소수점)

위에서 명시했듯 부동 소수점 표현 방식의 가장 큰 문제는 근사치 사용으로 인한 오차이다. 유효 범위 이외의 값은 가변적이므로 정확한 값을 보장할 수가 없는 것이다. 만약 금액이나 대출 이자와 같이 고정된 소수점까지 정확하게 관리해야 한다면? 절대절대 FLOAT나 DOUBLE 타입을 사용해서는 안된다. 이건 타협할 수 없는 조건에 가깝다. 이와 같이 정확한 표현을 위해 고정 소수점 방식을 사용할 수 있도록 돕는 타입이 바로 DECIMAL이다.

 

MySQL에서 DECIMAL 타입은 어떻게 숫자를 보존할 수 있을까? 바로, 숫자를 통으로 저장하는 게 아니라 자릿수의 숫자를 각각 따로 저장하는 방식이다. 예를 들어 똑같이 123.456이라는 숫자가 있다고 하자. 부동 소수점 방식에서는 이를 이진수로 변환한 뒤 통으로 바이트에 저장했지만, DECIMAL에서는 1/2/3/4/5/6 이런 식으로 쪼개서 저장한다는 말이다.

 

  • 1 -> 1
  • 2 -> 10
  • 3 -> 101
  • ...

따라서 각 숫자 하나씩 저장하는데 1/2 바이트

(1바이트 == 8비트, 1~9까지 표현하는데는 4비트만 있으면 되기 때문. 1(0001) ~ 9(1001))

가 필요하다. 그러니 세자릿수~네자릿수를 표현하는 데는 2바이트가 필요하다. 즉, (DECIMAL로 저장하는 숫자 자릿수 / 2)를 올림 처리한 만큼의 바이트 수가 필요한 것.

FLOAT(DOUBLE)과 DECIMAL: 성능 비교(계산 속도, 저장 효율성)

계산 속도(w/ Java)

MySQL 카테고리에 넣긴 했으나 실질적인 계산은 애플리케이션에서 이뤄지기 때문에 Java 코드로 FLOAT와 DECIMAL 계산 속도를 비교해봤다. 아래 코드는 임의의 Double과 BigDecimal 숫자를 1,000만 개 생성한 뒤, 그 총합을 계산하는 내용이다.

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class BigDecimalVsDoubleBenchmark {

    public static void main(String[] args) {
        final int N = 10_000_000; // Number of elements to sum
        List<BigDecimal> bigDecimalList = new ArrayList<>(N);
        List<Double> doubleList = new ArrayList<>(N);

        // Populate the lists with sample values
        for (int i = 0; i < N; i++) {
            Double dblValue = Math.random();
            BigDecimal bdValue = BigDecimal.valueOf(dblValue);

            bigDecimalList.add(bdValue);
            doubleList.add(dblValue);
        }
        BigDecimal bigDecimalSum = BigDecimal.ZERO;
        Double doubleSum = 0.0;
        long totalBigDecimalDuration = 0;
        long totalDoubleDuration = 0;
        // Benchmark BigDecimal sum
        for (int i = 0; i < 5; i++) {
            long startTime = System.nanoTime();
            bigDecimalSum = bigDecimalList.stream()
                                          .reduce(BigDecimal.ZERO, BigDecimal::add);
            long endTime = System.nanoTime();
            long bigDecimalDuration = (endTime - startTime) / 1_000_000; // Convert to milliseconds
            totalBigDecimalDuration += bigDecimalDuration;

            // Benchmark Double sum
            startTime = System.nanoTime();
            doubleSum = doubleList.stream()
                                  .reduce(0.0, Double::sum);
            endTime = System.nanoTime();
            long doubleDuration = (endTime - startTime) / 1_000_000; // Convert to milliseconds
            totalDoubleDuration += doubleDuration;
        }

        // Print results
        System.out.println("BigDecimal sum: " + bigDecimalSum + ", Time taken: " + totalBigDecimalDuration / 5 + " ms");
        System.out.println("Double sum: " + doubleSum + ", Time taken: " + totalDoubleDuration / 5 + " ms");
    }
}

N을 100만, 1000만으로 설정해 각각 5회 평균치 테스트 결과, 대략 4~5배 정도의 속도 차이가 나는 것을 확인할 수 있었다.

  • N = 1,000,000 → 4.4배
    • BigDecimal sum: 499860.1557756923649313224583, Time taken: 51 ms
    • Double sum: 499860.1557756844, Time taken: 8 ms
  • N = 10,000,000 → 5.6배
    • BigDecimal sum: 5000447.38614775657621909433263, Time taken: 316 ms
    • Double sum: 5000447.386147035, Time taken: 57 ms

저장 용량

일반적으로 DECIMAL 타입이 FLOAT(DOUBLE)보다는 더 많은 저장 공간을 요구하는 것은 사실이나, 우리는 소수점 자리를 2~3자리 정도로만 사용하기 때문에 큰 차이가 없다. 그리고 오늘날에 저장 용량당 비용은 매우 싼 편이기 때문에 페타바이트 단위의 데이터를 다루는 게 아닌 이상에야 저장 용량 때문에 DECIMAL을 포기할 이유는 없다.

결론: 핀테크 도메인에서 DECIMAL을 (거의) 반드시 써야 하는 이유

위에서 이미 언급했듯, 핀테크 분야에서는 금융을 다루기 때문에 금융 데이터의 정확성과 신뢰성을 보장하는 것이 계산 성능보다 압도적으로 중요하다. 따라서 일반적으로는 전부 DECIMAL 타입으로 금융 데이터를 저장한다고 생각하면 된다. 다만, HFT(High Frequency Trading)과 같이 연산 속도가 매우 중요한 특수 분야의 경우에는 FLOAT나 DOUBLE을 고려해볼 수 있을 것 같다. 하지만 이마저도 중간 계산 단계에서나 쓰일 뿐, 최종 저장은 반드시 DECIMAL 타입으로 변환해서 해야 한다.

Ref

반응형