infra
Platform

모듈 맵

[Database] 정밀한 데이터 타입(숫자·문자·날짜) 선택 기준

0 / 37 완료

펼치기
0 / 37 완료0%

Database · 04 / 37

[Database] 정밀한 데이터 타입(숫자·문자·날짜) 선택 기준

INT vs BIGINT, VARCHAR vs TEXT, TIMESTAMP vs DATE 등 타입 선택이 성능과 정확성에 미치는 영향을 이해합니다

🚨INCIDENT ALERT
HIGH

처음에는 VARCHAR 하나로 모든 값을 저장해도 동작하는 것처럼 보입니다. 하지만 금액, 시간, 상태값의 타입을 잘못 고르면 정렬·계산·인덱스에서 바로 문제가 납니다. 데이터 타입 선택은 저장 공간보다 서비스의 정확성과 유지보수성을 좌우합니다.

이번 챕터에서 배울 것

데이터 타입 선택이 성능, 정확성, 저장 공간에 어떤 영향을 미치는지 이해하고, 실무에서 자주 마주치는 타입 선택 실수를 피하는 방법을 익힙니다.

  • 1정수형 타입: SMALLINT ~ BIGINT 범위와 선택 기준
  • 2실수형: DECIMAL vs FLOAT 함정
  • 3문자형: CHAR vs VARCHAR vs TEXT
  • 4날짜/시간형과 타임존 처리
  • 5특수 타입: BOOLEAN, JSON, UUID, ENUM

데이터 타입 선택 기준 — 숫자, 문자, 날짜의 올바른 선택

입사 첫 달, 결제 금액 컬럼을 FLOAT으로 선언했다. 저장은 됐고, 조회도 됐고, 아무 문제 없어 보였다. 그런데 월말 정산 배치를 돌렸더니 합산 결과가 회계팀 숫자와 미묘하게 달랐다 — 1원, 2원씩. 원인을 찾는 데 이틀이 걸렸고, 결국 FLOAT의 부동소수점 오차가 수천 건에 걸쳐 누적된 것이었다. 컬럼 타입을 DECIMAL로 바꾸는 건 데이터 마이그레이션을 동반했고, 운영 시간 외에 작업해야 했다. 비슷한 일이 VARCHAR 쪽에서도 있었다 — 이름 컬럼을 TEXT로 선언해서 나중에 인덱스를 걸려니 길이 제한 오류가 났고, 글로벌 출시 후 TIMESTAMP 컬럼이 서울/뉴욕에서 9시간씩 틀어지는 사고도 겪었다. 타입 선택은 "저장만 되면 된다"의 문제가 아니라, 나중에 수정 비용이 얼마냐의 문제다. 이 모듈은 처음부터 올바른 타입을 고르는 판단 기준을 실수 사례와 함께 정리한다.

"어차피 다 저장되는 거 아닌가요?" 이런 생각으로 모든 컬럼을 VARCHAR(255)TEXT로 선언하는 경우가 있습니다. 하지만 잘못된 타입 선택은 저장 공간 낭비, 성능 저하, 심각한 경우 데이터 손실과 계산 오류로 이어집니다. 이 모듈에서는 각 타입의 내부 동작 원리와 선택 기준을 정확히 이해합니다.


💡개념

숫자와 문자 타입 — 잘못 선택하면 생기는 문제들

INT로 선언한 ID 컬럼이 21억을 넘었습니다. 더 이상 INSERT가 안 됩니다. 컬럼 타입을 BIGINT로 바꾸려면 수천만 건 테이블에서 ALTER가 몇 시간씩 걸리고, 그 사이 서비스는 멈춥니다. 처음부터 타입을 올바르게 선택하지 않으면 이 문제를 나중에 서비스 중단 없이 해결하기 어렵습니다.

숫자와 문자 타입 — 잘못 선택하면 생기는 문제들

정수형 타입 범위

타입크기최솟값최댓값사용 예시
SMALLINT2바이트-32,76832,767연령, 월, 소규모 카운터
INT / INTEGER4바이트-2,147,483,6482,147,483,647일반 ID, 카운터
BIGINT8바이트-9.2 × 10¹⁸9.2 × 10¹⁸대용량 ID, 타임스탬프(ms)
SERIAL4바이트12,147,483,647자동 증가 PK (INT + 시퀀스)
BIGSERIAL8바이트19.2 × 10¹⁸대용량 자동 증가 PK

INT Overflow 실제 사례

2012년 Pinterest는 INT PK overflow 문제를 경험했습니다. 21억 개 이상의 레코드가 쌓이면 INT가 오버플로우하기 때문입니다. 아래는 잘못된 선택과 올바른 선택을 비교한 예시입니다. 미래가 불확실하다면 처음부터 BIGINT를 사용하는 것이 안전합니다.

SQL
CREATE TABLE posts (
    id INT PRIMARY KEY
);
OUTPUT
실행 완료 또는 조회 결과가 표시됩니다.
🔍실행 후 확인할 것
  • 저장 타입숫자·날짜·문자 값이 의도한 타입으로 저장되는지 확인합니다.
  • 정렬 결과문자 숫자 정렬처럼 타입 선택으로 결과가 틀어지지 않는지 봅니다.
  • NULL 허용필수 값과 선택 값의 제약이 스키마에 반영됐는지 점검합니다.
SQL
CREATE TABLE posts (
    id BIGSERIAL PRIMARY KEY
);

실수형: DECIMAL vs FLOAT

FLOAT과 DOUBLE은 IEEE 754 부동소수점 방식으로 소수를 이진 근사값으로 표현합니다. 이 때문에 SELECT 1.1 + 2.2를 실행하면 3.3000000000000003처럼 정밀도 오류가 발생합니다. 금액 컬럼에는 절대 FLOAT을 사용하면 안 됩니다. DECIMAL(NUMERIC)은 정확한 십진수 연산을 보장합니다.

DECIMAL(precision, scale)에서 precision은 전체 유효 숫자 개수이고, scale은 소수점 이하 자릿수입니다. 예를 들어 DECIMAL(12, 2)는 최대 9999999999.99까지 저장할 수 있습니다.

FLOAT/DOUBLE은 과학적 계산이나 통계 분석처럼 근사값이 허용되는 경우에만 사용합니다.

SQL
CREATE TABLE payments (
    amount        DECIMAL(15, 2) NOT NULL,
    tax_rate      DECIMAL(5, 4),
    exchange_rate DECIMAL(10, 6)
);
SQL
CREATE TABLE sensor_readings (
    temperature DOUBLE PRECISION,
    humidity    FLOAT
);

FLOAT 또는 DOUBLE 타입으로 금액 컬럼을 선언하면 IEEE 754 부동소수점 연산의 특성상 미세한 정밀도 오류가 누적됩니다. 수천~수만 건의 합산에서 오차가 커져 회계 불일치로 이어집니다.

해결 방법: 금액 컬럼은 항상 DECIMAL 또는 NUMERIC 타입을 사용하세요. 소수점 이하 2자리가 필요하면 DECIMAL(15, 2)가 표준적인 선택입니다.

문자형 타입 선택

세 가지 문자형 타입의 저장 방식은 다음과 같습니다.

  • CHAR(n): 고정 길이. 항상 n바이트를 사용합니다. 데이터가 n보다 짧으면 나머지를 공백으로 채웁니다.
  • VARCHAR(n): 가변 길이. 최대 n글자까지 저장하며, 실제 문자열 길이 + 1~2바이트(길이 정보)만 사용합니다.
  • TEXT: 가변 길이. 길이 제한이 없습니다.

길이가 항상 고정된 데이터(국가 코드, 통화 코드 등)에는 CHAR를, 최대 길이가 정해진 가변 데이터에는 VARCHAR를, 길이 제한 없는 긴 텍스트에는 TEXT를 사용합니다.

TEXT 컬럼에는 인덱스 크기 제한이 있으므로 전체 텍스트 검색이 필요하다면 전문 검색 기능을 사용해야 합니다.

SQL
CREATE TABLE products (
    country_code CHAR(2)       NOT NULL,
    currency     CHAR(3)       NOT NULL,
    name         VARCHAR(100)  NOT NULL,
    email        VARCHAR(255)  NOT NULL,
    title        VARCHAR(500)  NOT NULL,
    description  TEXT,
    content      TEXT
);

VARCHAR 길이 선택 가이드

아래는 실무에서 각 필드 유형에 권장되는 VARCHAR 크기입니다. RFC 표준이나 알고리즘 출력 길이가 고정된 경우는 그 값을 그대로 따릅니다.

필드타입근거
emailVARCHAR(255)RFC 5321 표준 최대 길이
usernameVARCHAR(50)일반적인 서비스 제한
password_hashCHAR(60)bcrypt 해시는 항상 60자
phoneVARCHAR(20)국제 전화번호 포함
urlVARCHAR(2048)URL 최대 길이 고려
ip_addressVARCHAR(45)IPv6 포함 최대 45자
country_codeCHAR(2)ISO 3166-1
currency_codeCHAR(3)ISO 4217
zip_codeVARCHAR(10)국가별 우편번호 최대 길이
💡개념

날짜/시간 타입과 타임존 함정 — TIMESTAMP vs DATETIME

한국 서버에서 정상적으로 저장된 시간이 미국 서버로 마이그레이션하자 9시간이 틀어졌습니다. DATETIME을 쓴 것이 원인이었습니다. TIMESTAMPDATETIME 중 어느 것을 쓰느냐에 따라 타임존 변환 동작이 완전히 다릅니다. 잘못 선택하면 글로벌 서비스에서 반드시 시간 버그가 발생합니다.

날짜/시간 타입과 타임존 함정 — TIMESTAMP vs DATETIME

날짜/시간 타입 비교

타입크기범위타임존
DATE4바이트4713 BC ~ 5874897 AD없음
TIME8바이트00:00:00 ~ 24:00:00없음
TIMESTAMP8바이트4713 BC ~ 294276 AD없음
TIMESTAMPTZ8바이트4713 BC ~ 294276 ADUTC 변환 저장
INTERVAL16바이트±178,000,000년해당없음

TIMESTAMP vs TIMESTAMPTZ 차이

TIMESTAMP는 타임존 정보가 없어 입력값을 그대로 저장합니다. 서울(UTC+9) 세션에서 15:00:00을 저장하면 그냥 15:00:00이 저장되고, 이 값이 어느 타임존의 15시인지 DB는 알 수 없습니다.

TIMESTAMPTZ(TIMESTAMP WITH TIME ZONE)는 입력값을 UTC로 변환해 저장하고, 조회 시 세션 타임존에 맞게 자동으로 변환해 돌려줍니다. 같은 15:00:00 KST 값이 내부적으로 06:00:00 UTC로 저장되고, 뉴욕 타임존으로 조회하면 02:00:00 EDT로 변환됩니다.

SQL
INSERT INTO events (created_at_tz)
VALUES ('2026-04-07 15:00:00');
SQL
SELECT created_at_tz FROM events;

TIMESTAMP 타입을 사용하면 저장된 값에 타임존 정보가 없습니다. 서버 타임존을 변경하거나 글로벌 사용자가 늘어나면 동일한 데이터가 지역마다 다르게 해석됩니다.

해결 방법: 레코드 생성/수정 시간은 항상 TIMESTAMPTZ를 사용하고 UTC로 저장하세요. 생년월일처럼 순수한 날짜 데이터는 DATE를 사용합니다. 글로벌 서비스라면 TIMESTAMP는 사용하지 않는 것이 원칙입니다.

날짜 타입 실무 패턴

생년월일이나 이벤트 날짜처럼 특정 날짜만 필요한 경우에는 DATE를 사용합니다. 레코드의 생성/수정 시각에는 TIMESTAMPTZ가 맞습니다. updated_at은 UPDATE가 발생할 때마다 자동으로 갱신되도록 트리거를 걸어 두는 것이 일반적입니다.

SQL
CREATE TABLE users (
    birthdate  DATE,
    joined_on  DATE NOT NULL DEFAULT CURRENT_DATE
);
SQL
CREATE TABLE products (
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

날짜 연산에는 EXTRACTINTERVAL을 활용합니다. 기간별 집계에는 DATE_TRUNC로 날짜를 월/주 단위로 잘라 GROUP BY에 사용합니다.

SQL
SELECT
    EXTRACT(YEAR FROM age(birthdate)) AS age,
    birthdate + INTERVAL '18 years'   AS adult_date
FROM users;
SQL
SELECT
    DATE_TRUNC('month', created_at) AS month,
    COUNT(*)                        AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;

특수 타입 정리

BOOLEANTRUE, FALSE, NULL 세 가지 값을 가집니다. 기본값을 반드시 명시하는 것이 좋습니다.

UUID는 분산 환경에서 충돌 없이 전역 고유 ID가 필요할 때 사용합니다. 16바이트(INT의 4배)이므로 내부 PK는 BIGINT, 외부 노출용으로 UUID를 별도 컬럼에 두는 패턴을 권장합니다.

JSONB는 JSON을 바이너리로 저장해 인덱스를 지원하고 조회 속도가 빠릅니다. JSON은 텍스트로 저장하며 입력 순서를 보존하지만 인덱스를 사용할 수 없어 일반적으로 JSONB를 권장합니다.

ENUM은 컬럼 값을 특정 문자열 집합으로 제한합니다. 단, 값 추가/변경이 번거롭기 때문에 CHECK 제약이 더 유연한 대안이 될 수 있습니다.

SQL
CREATE TABLE users (
    is_active   BOOLEAN NOT NULL DEFAULT TRUE,
    is_verified BOOLEAN NOT NULL DEFAULT FALSE,
    metadata    JSONB   DEFAULT '{}'
);
SQL
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'delivered', 'cancelled');

CREATE TABLE orders (
    status order_status NOT NULL DEFAULT 'pending'
);
💼
실무 맥락결제 서비스 설계 시 금액·시간 컬럼 타입을 결정해야 하는 상황
현업 패턴

결제 서비스를 개발할 때 amount 컬럼을 FLOAT으로 선언하면 수천 건의 정산 합산에서 1~2원의 오차가 발생해 회계 불일치 문제가 생깁니다. 실무에서는 DECIMAL(15, 2)를 표준으로 사용합니다.

마찬가지로 created_atTIMESTAMP로 선언한 서비스가 글로벌로 확장될 때 기존 데이터 마이그레이션 비용이 발생합니다. 처음부터 TIMESTAMPTZ를 사용하면 DB가 타임존 변환을 자동으로 처리하므로 애플리케이션 레이어에서 별도 변환 로직이 필요 없습니다.

이 두 가지 타입 선택만 올바르게 해도 나중에 발생하는 대규모 데이터 마이그레이션을 예방할 수 있습니다.

다음 모듈에서는 PK, FK 제약조건과 CASCADE 삭제 설정이 데이터 정합성에 미치는 영향을 다룹니다.

지식 확인

퀴즈 — 5문제

Q1

이커머스 서비스에서 상품 가격을 저장할 컬럼 타입으로 FLOAT 대신 DECIMAL(10,2)를 선택해야 하는 이유는?

Q2

CHAR(10)과 VARCHAR(10)의 차이점으로 올바른 것은?

Q3

타임존이 있는 서비스에서 날짜/시간 저장 시 가장 권장되는 전략은?

Q4

UUID를 PK로 사용할 때의 단점은?

Q5

금액 데이터 저장 시 FLOAT 또는 DOUBLE을 피해야 하는 이유는?

0 / 5 답변

🧪 실습으로 확인하기

PostgreSQL 설치 및 기본 설정

초급

Ubuntu 서버에 PostgreSQL을 설치하고, 데이터베이스와 사용자를 생성한 뒤 외부 접속이 가능하도록 설정한다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

database입문 · 50
[Database] 제1·2·3정규화와 역정규화(De-normalization) 실전 적용 기준
Database 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점