데이터 베이스/설계 1편, 현대적 데이터 모델링 완전 정복

Ch10. 물리적 모델링 - 문자, 숫자, PK 타입

webmaster 2026. 7. 1. 23:27

논리적 모델의 "속성"을 물리적 모델의 "컬럼"으로 바꿀 , 가장 먼저 해야 일은 컬럼에 어떤 종류의 데이터가 들어갈지 정의하는 것이다. 이것이 바로 데이터 타입 지정하는 작업이다.

 

데이터 타입을 잘못 선택하면 다음과 같은 문제가 발생한다.

  • 저장 공간 낭비
    • 필요보다 큰 데이터 타입을 사용하면 디스크 공간이 낭비된다.
    • 데이터가 수백만, 수천만 건이 되면 이 낭비는 무시할 수 없는 수준이 된다.
  • 성능 저하
    • 데이터베이스는 디스크에서 메모리로 데이터를 읽어와 처리한다.
    • 데이터 타입이 크면 한 번에 읽어올 수 있는 데이터의 양이 줄어든다.
      • 이는 더 많은 I/O(입출력)를 유발하고, 결국 쿼리 속도를 느리게 만든다.
  • 데이터 무결성 훼손
    • 데이터의 성격에 맞지 않는 타입을 사용하면 잘못된 데이터가 입력될 수 있다.
    • 예를 들어, 날짜를 문자열(VARCHAR)로 저장하면 "2025-08-21" 뿐만 아니라 "내일", "어제" 같은 엉뚱한 값도 들어갈 수 있다.

따라서 우리는 데이터의 특성과 범위를 정확히 파악하고, 그에 맞는 최적의 데이터 타입 선택해야 한다. 이제 MySQL에서 자주 사용하는 주요 데이터 타입들을 하나씩 알아보자.

 

문자열 타입

문자열을 저장하는 타입은 가변 길이와 고정 길이로 나뉜다.

  • VARCHAR(M)
    • 가변 길이 문자열. "M"은 저장할 수 있는 최대 길이를 의미한다.
    • 실제 저장되는 데이터의 길이에 따라 저장 공간의 크기가 달라진다.
    • 예를 들어 VARCHAR(100)에 "hello"(5글자)를 저장하면, 실제로는 5글자에 해당하는 공간 + 길이 정보(1~2바이트)만 사용한다.
      • 최대 바이트 길이가 255 이하이면 1바이트, 그 초과이면 2바이트를 사용한다.)
    • 장점: 공간 효율이 좋다.
    • 단점: 길이가 변하기 때문에 데이터 수정 시 추가적인 작업이 필요할 수 있다.
    • 용도: 이름, 제목, 주소 등 대부분의 문자열 데이터에 사용된다.
  • CHAR(M)
    • 고정 길이 문자열. "M"은 고정된 길이를 의미한다.
    • CHAR(10)에 "hello"(5글자)를 저장하면, 나머지 5글자는 공백으로 채워져 무조건 10글자의 공간을 차지한다.
    • 장점: 길이가 고정되어 있어 데이터 처리 속도가 "VARCHAR" 보다 약간 빠를 수 있다.
    • 단점: 공간 낭비가 심하다.
    • 용도: 주민등록번호, 전화번호, 성별(M/F)처럼 길이가 항상 고정된 데이터에 사용할 수 있다.
  • TEXT
    • 매우 긴 텍스트를 저장할 때 사용한다.
    • 게시물의 본문, 상품의 상세 설명 등에 사용된다. (e.g., TEXT는 약 6만 5 바이트, MEDIUMTEXT는 약 1600만 바이트, LONGTEXT는 약 42억 바이트)

실무 가이드

  • "일단 VARCHAR를 써라."
    •  대부분의 경우 VARCHAR가 가장 합리적인 선택이다.
    • 길이가 고정된 데이터라도 그 길이가 바뀔 가능성이 있다면 VARCHAR를 쓰는 것이 더 안전하다.
  • VARCHAR의 길이는 얼마나 잡아야 할까?
    • 너무 길게 잡으면 메모리 사용량이 늘어날 수 있고, 너무 짧게 잡으면 데이터가 잘리는 문제가 생긴다.
    • 저장될 데이터의 평균적인 길이와 최대 길이를 고려하여 합리적으로 설정해야 한다.
    • 예를 들어, 사용자 이름은 50자, 이메일 주소는 100자 정도로 잡는 것이 일반적이다
DROP TABLE IF EXISTS member_sample;

CREATE TABLE member_sample (
    member_id BIGINT PRIMARY KEY,
    email VARCHAR(100), -- 이메일 주소는 길이가 다양하다.
    name VARCHAR(50), -- 이름도 길이가 다양하다.
    gender CHAR(1), -- 성별은 'M' 또는 'F'로 길이가 1로 고정된다.
    introduction TEXT -- 자기소개는 매우 길어질 수 있다.
);
INSERT INTO member_sample (member_id, email, name, gender, introduction)
VALUES (1, 'test@example.com','션', 'M','안녕하세요. 데이터베이스를 배우는 션입니다.');

 

[심화] VARCHAR 길이는 메모리 사용량에 영향을 줄까?

 

VARCHAR 가변 길이인데 길이를 크게 잡으면 메모리 사용량이 늘어날까? 디스크 저장 공간과 메모리 사용은 다른 이야기이기 때문이다.

 

결론부터 말하면, MySQL 쿼리를 처리하기 위해 메모리에 임시 공간을 할당할  VARCHAR 설정된 최대 길이를 기준으로 삼는 경우가 많다. 예를 들어, 우리가 "ORDER BY" "GROUP BY" 같은 정렬이나 그룹화 작업을 수행한다고 생각해보자. MySQL 효율적인 작업을 위해 디스크에 있는 데이터를 메모리로 가져와서 중간 결과물을 저장할 임시 테이블(temporary table)을 만들 있다.

 

이때, 메모리에 만들어지는 임시 테이블의 컬럼 크기는 원본 테이블의 VARCHAR 최대 길이를 따라간다.

  • name VARCHAR(50) 컬럼을 정렬한다면: MySQL은 메모리에 한 사람의 이름을 저장하기 위해 최대 50글자를 담을 수 있는 공간을 할당한다.
    • 실제 이름이 Kim(3글자)이더라도, 정렬 중 자리가 바뀔 수 있으므로 넉넉하게 최대 크기로 잡는 것이다.
  • name VARCHAR(4000) 컬럼을 정렬한다면: MySQL은 같은 원리로 메모리에 최대 4000글자를 담을 수 있는 공간을 할당한다.

이제 수백만 건의 회원 데이터를 정렬한다고 상상해 보자. VARCHAR(50) 일 때와 VARCHAR(4000) 일 때, 정렬 작업을 위해 필요한 총 메모리의 양은 어마어마한 차이가 난다. 만약 할당해야 메모리가 너무 커서 서버의 물리적인 메모리(RAM) 용량을 초과하면, MySQL 디스크를 임시 공간으로 사용하기 시작한다. 디스크 I/O 메모리 I/O보다 수천 배에서 수만 느리기 때문에, 이는 곧바로 쿼리 성능의 급격한 저하로 이어진다. 따라서 VARCHAR 길이를 설정할 때는 "혹시 모르니 그냥 엄청 크게 잡자" 아니라, " 데이터가 가질 있는 합리적인 최대 길이가 얼마일까?" 신중하게 고민해야 한다. 이것이 바로 디스크 공간 효율뿐만 아니라, 시스템 전체의 성능을 생각하는 좋은 데이터베이스 설계자의 자세이다.

 

숫자 타입

정수 타입

정수를 저장할 사용하며, 저장할 있는 값의 범위에 따라 여러 타입으로 나뉜다.

타입  저장 공간 (Bytes)  최소값  최대값  용도 예시
TINYINT` 1  -128  127  나이, 상태 코드
SMALLINT  2  -32,768  32,767  작은 범위의 개수
MEDIUMINT  3  -8,388,608  8,388,607
INT 4  -2,147,483,648  2,147,483,647  일반적인 ID, 조회수,
재고 수량
BIGINT  8  약 -922경  약 922경  매우 큰 ID (주문 번호
등)

 

실무 가이드

  • "TINYINT"는 상태 값(예: 0=대기, 1=처리 중, 2=완료)이나 한정된 범위의 숫자에 사용하면 공간을 매우 효율적으로 쓸 수 있다.
  • 일반적인 게시물 ID, 회원 ID 등은 "INT"로 충분한 경우가 많다.
    • 하지만 양수만 저장하는 경우 "UNSIGNED" 옵션을 사용하면 저장 범위를 2배로 늘릴 수 있다.
      • 예를 들어 "INT UNSIGNED"는 0부터 약 42억까지 저장할 수 있어, 음수가 필요 없는 ID 값에 사용하기 좋다.
  • 앞으로 서비스가 엄청나게 커질 것을 대비해 무조건 "BIGINT"를 사용하는 경우가 있는데, 이는 신중해야 한다.
    • 트래픽이 많지 않은 관리자 페이지의 ID 등은 "INT"로도 충분하다.
    • 하지만 사용자의 주문 ID처럼 데이터가 폭발적으로 증가할 것이 확실하다면 처음부터 "BIGINT"를 고려하는 것이 좋다.
    • 나중에 "INT"에서 "BIGINT"로 바꾸는 작업은 매우 고통스럽기 때문이다.

 

소수 타입

소수점이 있는 숫자를 저장할 사용한다.

  • DECIMAL(M, D)
    • 고정 소수점 타입. 금융 계산처럼 정확한 소수점 계산이 필요할 때 반드시 사용해야 한다.
    • "M"은 총 자릿수, "D"는 소수점 이하 자릿수를 의미한다.
      • 예를 들어 "DECIMAL(10, 2)"는 정수 부분 8자리, 소수 부분 2자리까지 총 10자리의 숫자를 정확하게 저장할 수 있다.
  • DOUBLE 또는 FLOAT
    • 부동 소수점 타입. 과학 계산이나 근사치가 허용되는 빠른 계산에 사용된다.
    • 하지만 소수점 계산 시 미세한 오차가 발생할 수 있어, 돈과 관련된 계산에는 절대 사용하면 안 된다.

예제: 상품 가격과 평점을 저장하는 테이블을 만들어보자.

DROP TABLE IF EXISTS product_sample;

CREATE TABLE product_sample (
    product_id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10, 2), -- 99999999.99 까지 저장 가능
    rating DOUBLE -- 평점은 근사치로 저장해도 무방
);

INSERT INTO product_sample (product_id, name, price, rating)
VALUES (1, '프리미엄 키보드', 159000.00, 4.8);

INSERT INTO product_sample (product_id, name, price, rating)
VALUES (2, '고급 마우스', 89500.50, 4.9);
  • "price"에 달러와 같은 소수점을 사용하는 통화라면 "DECIMAL"을 사용해서 정확도를 보장해야 한다.
  • "rating"은 약간의 오차가 있어도 괜찮으므로 "DOUBLE"을 사용했다.

 

PK 타입 선정: INT vs BIGINT

기본 (PK) 자동 증가(Auto Increment)하는 숫자로 정했다면, 이제 어떤 숫자 타입을 쓸지 결정해야 한다. 현실적으로 선택지는 "INT" "BIGINT" 하나로 좁혀진다.

 

과거에는 "INT" PK 표준처럼 사용되었다. "INT" "UNSIGNED" 옵션을 적용하면 0부터 42억까지의 숫자를 저장할 있다. 42억이라는 숫자는 어지간한 서비스에서는 전부 소진하기 어려운 매우 숫자이므로, 대부분의 경우에 충분했다.

 

하지만 현대의 서비스, 특히 우리가 만들 쇼핑몰처럼 성공을 목표로 하는 서비스는 데이터 증가 속도를 예측하기 어렵다. 회원이 수천만 명이 되고, 회원이 여러 개의 주문을 생성하며, 주문마다 여러 개의 주문 상품 데이터가 쌓인다고 생각해보자. 핵심적인 데이터인 orders order_item 테이블은 수십억 건을 넘어설 가능성이 충분하다.

 

INT vs BIGINT 성능과 저장 공간

그렇다면 고민하는가? 처음부터 가장 "BIGINT" 사용하면 모든 문제가 해결되지 않을까? 맞는 말이면서도 틀린 말이다. "BIGINT" "INT" 비해 배의 저장 공간(8 bytes vs 4 bytes) 사용한다. 이것이 단순히 디스크 공간만 차지하는 문제에서 그치지 않는다. 데이터베이스에서 성능에 가장 영향을 미치는 것은 I/O(디스크 입출력).

 

데이터베이스는 인덱스라는 자료구조를 통해 데이터를 빠르게 찾는다. 이때 인덱스 정보도 결국 디스크에 저장되어 있고, 필요할 메모리로 읽어와야 한다. PK 가장 중요한 인덱스다. 데이터 타입의 크기가 작을수록, 번에 메모리에 읽어올 있는 인덱스 데이터의 양이 많아진다. , 적은 I/O 원하는 데이터를 찾을 가능성이 커진다는 의미다. 따라서 "INT" 사용하는 것이 "BIGINT" 사용하는 것보다 이론적으로 빠르고 효율적이다.

 

BIGINT PK 사용하는 이유

지금까지 이야기한 내용을 합리적으로 생각해보면 다음과 같은 가이드라인이 떠오를 것이다.

  • 회원, 상품, 주문, 결제 등 서비스의 핵심 데이터
    •  사용자가 계속해서 만들어내는 데이터, 즉 앞으로 얼마나 늘어날지 예측하기 어려운 테이블의 PK는 고민하지 말고 "BIGINT"를사용한다.
      • 미래의 위험을 미리 방지하는 것이 현명한 선택이다.
    • 우리 쇼핑몰 예제의 "member_id", "product_id", "order_id"가 모두 "BIGINT"인 이유다. (국내서비스라면 회원의 경우에는 "INT"를 고려할 수 있다.)
  • 카테고리, 상태 코드 등 내부적으로 사용하는 데이터
    •  데이터의 최대 개수가 수백, 수천 개 수준으로 명확하게 예측가능한 테이블(예: 상품 카테고리, 주문 상태 코드 테이블)은 "INT"를 사용하는 것이 효율적이다.

그럼에도 불구하고, 결론부터 말하면 실무에서는 핵심 테이블의 PKBIGINT 사용을 권장한다.

 

INT BIGINT 저장 공간 차이 분석

"BIGINT" "INT"보다 2배의 공간을 사용하는 것은 사실이다. 하지만 이것이 실제로 얼마나 차이를 만드는지 표로 확인해보자.

데이터 건수  INT 저장 공간 (4 bytes)  BIGINT 저장 공간 (8 bytes)  차이
1 건  4 Bytes  8 Bytes  4 Bytes
10 건  40 Bytes  80 Bytes  40 Bytes
100 건  400 Bytes  800 Bytes  400 Bytes
1,000 건  4 KB  8 KB  4 KB
10,000 건  39 KB  78 KB  39 KB
100,000 건  390 KB  781 KB  391 KB
1,000,000 건  3.8 MB  7.6 MB  3.8 MB
10,000,000 건  38.1 MB  76.3 MB  38.1 MB
100,000,000 건  381.5 MB 762.9 MB  381.5 MB
1,000,000,000 건  3.7 GB  7.4 GB  3.7 GB

 

  • 쇼핑몰 판매자가 1000명이라면 "INT"와 "BIGINT" 차이는 겨우 "4 KB"이다.
  • 카테고리가 1만 건이라면 "INT"와 "BIGINT" 선택의 차이는 겨우 "39 KB" 차이다.
  • 상품 데이터가 100만 건이라면 "INT"와 "BIGINT" 선택의 용량 차이는 약 "3.8 MB"에 불과하다.
    • 이는 최신 스마트폰의 사진 한 장보다 작은 용량이다.
  • 참고로 데이터베이스 내부의 실제 부가 비용은 더 들 수 있다.

1 정도 되어야 "400MB" 어느정도 의미있는 수준의 차이가 나오기 시작한다. 정도 차이는 현대의 스토리지 기술 수준에서는 충분히 감당할 있는 수준이다. 게다가 데이터가 1 건에 도달했다는 자체가 이미 서비스가 엄청나게 성장했다는 증거이며, "INT" 사용했다면 시점에서는 최대 저장 용량 초과 위험 때문에 "BIGINT"로의 변경을 심각하게 고려해야 것이다. , "BIGINT" 사용함으로써 잃는 저장 공간의 손해는 매우 미미한 반면, 얻게 되는 "칙의 단순함", "미래 확장성", "관리의 용이성"이라는 이점은 매우 크다.

 

1. 미래 확장성

"INT" 사용해서 얻는 약간의 성능상 이점보다, 나중에 "INT" 저장 범위를 초과했을 발생하는 재앙적인 비용 훨씬 크다. 서비스가 한창 성장하는 와중에 PK 타입을 "INT"에서 "BIGINT" 변경하는 작업(Migration) 상상 이상으로 고통스럽다. 서비스 전체를 중단해야 수도 있고, 데이터 정합성이 깨질 위험도 크다.

 

실무에서는 다른 관점, 바로 일관성 가치를 매우 높게 평가한다. 데이터가 적을 것이라 예상되는 테이블에도 "BIGINT" 적용하는 것이 장기적으로 나은 선택일 있다.

 

일관성이 주는 강력함

프로젝트의 모든 테이블 PK "BIGINT" 통일하면 다음과 같은 장점을 얻을 있다.

  • 고민의 제거와 개발 생산성 향상
    • 개발자는 새로운 테이블을 설계할 때마다 "이 테이블은 데이터가 많이 쌓일까? INT로 충분할까?" 고민할 필요가 없다.
    • PK "BIGINT"라는 단순하고 명확한 규칙은 개발자의 인지 부하를 줄여주고, 중요한 비즈니스 로직에 집중하게 만든다.
  • 예측 실패의 위험 방지
    • 지금은 데이터가 몇 개 없을 것 같은 "카테고리" 테이블이, 서비스가 고도화되면서 사용자가 직접 카테고리를 생성하고 태그처럼 사용하는 기능으로 확장될 수도 있다.
    • 초기의 예측이 틀렸을 때 "INT"에서 "BIGINT"로 변경하는 비용은 매우 크다.
    • 일관된 "BIGINT" 정책은 이러한 예측 실패의 위험을 원천적으로 차단하는 "보험"과 같다.
  • 외래키 관리의 단순함
    • orders 테이블의 "member_id"는 member 테이블의 "member_id"를 참조하는 외래키(FK)다.
      • FK는 참조하는 PK와 반드시 데이터 타입이 같아야 한다.
      • 만약 어떤 PK는 "INT"이고 다른 PK는 "BIGINT"라면, FK를 설정할 때마다 참조하는 테이블의 PK 타입을 확인해야 하는 번거로움이 생긴다.
      • 이는 실수로 이어질 가능성을 높인다. 모든 PK를 "BIGINT"로 통일하면, 모든 FK 또한 "BIGINT"로 만들면 되므로 실수를 줄 일 수 있다.

따라서 많은 최근 실무 현장에서는 성능이나 저장 공간의 미미한 차이보다는, 장기적인 안정성과 유지보수 편의성을 위해 모든 테이블의 PK "BIGINT" 통일하는 전략을 선호한다.