데이터 타입과 정밀도
값의 성격에 맞는 타입을 고르지 않으면 계산 결과가 조용히 틀어진다
금액에 부동소수점을 쓰면
부동소수점은 2진수로 저장하므로 10진 소수를 정확히 표현하지 못합니다.
| 계산 | 부동소수점 결과 |
|---|
| 0.1 + 0.2 | 0.30000000000000004 |
| 0.1 을 10번 더하기 | 0.9999999999999999 |
| 큰 금액 + 작은 금액 | 작은 쪽이 흡수되어 사라진다 |
한 건이면 눈에 안 보이지만 수백만 건을 합치면 오차가 쌓입니다. 정산에서 몇 원씩 안 맞는 일이 여기서 생깁니다.
| 타입 | 성격 | 쓰는 곳 |
|---|
| DECIMAL, NUMERIC | 10진수를 정확히 저장 | 금액, 수량, 비율 |
| FLOAT, DOUBLE | 근사값. 빠르고 범위가 넓다 | 좌표, 측정값, 통계 |
| INTEGER | 정수 | 개수, 식별자 |
| 정수로 최소 단위 저장 | 원 단위나 센트 단위로 | 금액을 정수로 다루는 방식 |
마지막 방식도 널리 쓰입니다. 금액을 센트 단위 정수로 저장하면 소수 계산이 아예 없습니다. 다만 나누기가 있으면 반올림 규칙을 정해야 합니다.
원화라면
원화는 소수점을 쓰지 않으므로 정수로 충분합니다. 다만 이런 경우를 고려합니다.
| 상황 | 판단 |
|---|
| 할인율 계산 중간값 | 소수가 생긴다. 반올림 시점을 정해야 한다 |
| 외화 결제 | 소수 둘째 자리가 필요하다 |
| 세금 계산 | 원 단위 절사 규칙이 있다 |
시각
| 타입 | 저장하는 것 | 주의 |
|---|
| TIMESTAMP WITH TIME ZONE | 시점을 UTC 로 저장하고 조회 시 변환 | 대개 이것을 쓴다 |
| TIMESTAMP WITHOUT TIME ZONE | 시계에 적힌 값만. 어느 지역인지 모른다 | 지역이 하나여도 위험하다 |
| DATE | 날짜만 | 시간대에 따라 날짜가 달라진다 |
시간대를 저장하지 않으면 서버 위치가 바뀌거나 서머타임이 적용될 때 값의 의미가 달라집니다. 저장은 UTC 로, 표시할 때 사용자 시간대로 바꾸는 것이 기본입니다.
문자열
| 항목 | 판단 |
|---|
| 길이 제한 | 의미가 있으면 제한한다. 없으면 TEXT 로 |
| 가변과 고정 | 대개 가변. 고정 길이는 공백을 채운다 |
| 인코딩 | UTF-8. 이모지는 4바이트를 쓰므로 저장 방식을 확인한다 |
| 식별자 | 숫자로만 이뤄져도 전화번호나 우편번호는 문자열로 |
마지막이 자주 틀립니다. 전화번호를 정수로 저장하면 앞의 0 이 사라집니다.
Q.금액을 저장할 때 어떤 타입을 쓰나요?
DECIMAL(또는 NUMERIC)이나 최소 단위 정수를 씁니다. 부동소수점은 쓰지 않습니다.
| 타입 | 결과 |
|---|
| FLOAT, DOUBLE | 0.1 + 0.2 가 0.3 이 아니다. 합칠 때마다 오차가 쌓인다 |
| DECIMAL | 10진수를 정확히 저장한다. 계산이 정확하다 |
| 정수(최소 단위) | 소수 계산이 없다. 원이나 센트 단위로 저장 |
부동소수점이 위험한 이유는 2진수로 저장하기 때문입니다. 0.1 을 2진수로 정확히 쓸 수 없어서 근사값이 들어갑니다. 한 건에서는 안 보이지만 수백만 건을 합치면 어긋납니다.
원화라면 정수로 충분하지만, 이런 경우를 함께 봅니다.
| 상황 | 필요한 것 |
|---|
| 할인율 계산 | 중간에 소수가 생긴다. 반올림 시점을 정한다 |
| 외화 | 소수 둘째 자리 |
| 부가세 | 절사 규칙 |
흔한 실수: 애플리케이션 언어에서 부동소수점으로 받아 계산하는 것. DB 를 DECIMAL 로 해도 코드에서 실수형으로 읽어 계산하면 같은 문제가 생깁니다. 언어의 십진 타입을 함께 써야 합니다.
Q.시각을 저장할 때 시간대를 왜 함께 다뤄야 하나요?
시간대 없이 저장하면 그 값이 어느 시점을 가리키는지 알 수 없습니다.
| 타입 | 저장하는 것 | 문제 |
|---|
| 시간대 포함 | 시점을 UTC 로 저장 | 없다. 조회 시 원하는 지역으로 변환 |
| 시간대 없음 | 시계에 적힌 값만 | 서버 위치가 바뀌면 의미가 달라진다 |
문제가 드러나는 상황들입니다.
| 상황 | 결과 |
|---|
| 서버를 다른 지역으로 이전 | 같은 값이 다른 시점을 의미하게 된다 |
| 서머타임 | 같은 시각이 두 번 나타나거나 존재하지 않는 시각이 생긴다 |
| 여러 지역 사용자 | 누구 기준의 시각인지 모른다 |
| 지역별 집계 | 하루의 경계가 달라 숫자가 안 맞는다 |
원칙은 저장은 UTC, 표시는 사용자 시간대입니다. 그리고 날짜 경계로 집계할 때는 어느 시간대 기준인지 명시해야 합니다.
흔한 실수: 서비스가 한국에서만 쓰이니 시간대가 필요 없다고 판단하는 것. 서버가 UTC 로 도는 컨테이너에 배포되면 그때 어긋나고, 이미 저장된 값은 어느 기준인지 알 수 없어 되돌리기 어렵습니다.
Q.전화번호나 우편번호를 정수로 저장하면 어떤 문제가 있나요?
앞자리 0 이 사라지고, 숫자로서의 연산이 의미가 없습니다.
| 문제 | 예 |
|---|
| 앞의 0 손실 | 010-1234-5678 이 1012345678 이 된다 |
| 구분자 손실 | 하이픈이나 공백을 담을 수 없다 |
| 국가 번호 | +82 의 기호를 담을 수 없다 |
| 자릿수 초과 | 국제번호가 정수 범위를 넘을 수 있다 |
| 무의미한 연산 | 전화번호를 더하거나 평균낼 일이 없다 |
판단 기준이 있습니다. 그 값으로 계산을 하는가입니다. 계산하지 않는 숫자는 문자열입니다.
| 값 | 타입 | 이유 |
|---|
| 나이, 수량, 금액 | 숫자 | 더하고 비교한다 |
| 전화번호, 우편번호 | 문자열 | 계산하지 않는다. 앞의 0 이 의미가 있다 |
| 주민번호, 사업자번호 | 문자열 | 같다. 그리고 암호화 대상이다 |
| 버전 문자열 | 문자열 또는 분리 저장 | 1.10 이 1.9 보다 크다 |
흔한 실수: 저장은 문자열로 하면서 형식을 통일하지 않는 것. 하이픈이 있는 것과 없는 것이 섞이면 검색과 중복 확인이 어긋납니다. 저장 시 정규화하고 표시할 때 형식을 붙이는 편이 낫습니다.
Q.큰 수와 작은 수를 함께 다룰 때 무엇을 주의하나요?
부동소수점은 큰 수에 작은 수를 더하면 작은 쪽이 흡수되어 사라집니다.
| 계산 | 부동소수점 결과 |
|---|
| 1,000,000,000 + 0.0001 | 작은 쪽이 반영되지 않을 수 있다 |
| 작은 값을 반복 누적 | 오차가 쌓인다 |
| 큰 값끼리 빼기 | 유효 자릿수가 줄어 정밀도를 잃는다 |
원인은 부동소수점이 상대적 정밀도를 가진다는 것입니다. 자릿수가 정해져 있어서, 큰 값을 표현하는 데 자릿수를 다 쓰면 작은 값을 담을 자리가 없습니다.
| 대응 | 내용 |
|---|
| DECIMAL 을 쓴다 | 자릿수를 명시해 정확하게 |
| 정수 단위를 바꾼다 | 최소 단위 정수로 저장 |
| 합산 순서를 정한다 | 작은 값끼리 먼저 더하고 큰 값에 합친다 |
| 누적을 DB 에 맡긴다 | SUM 을 DECIMAL 컬럼에서 수행 |
세 번째는 통계 계산에서 실제로 쓰는 기법입니다. 정렬해서 작은 것부터 더하면 오차가 줄어듭니다.
흔한 실수: 테스트 데이터가 작아서 문제를 못 보는 것. 개발에서 몇 건으로 검증하면 오차가 드러나지 않고, 운영에서 수백만 건을 합칠 때 나타납니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
데이터베이스 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.