Database ‐ 물리적 모델링 - woojin-playground/Backend-PlayGround GitHub Wiki

Database - Physical Data Modeling

테이블과 컬럼 변환 규칙 - 기본 규칙

  • 영어 사용 : 데이터베이스 객체의 이름은 영어를 사용하는 것이 국제 표준이다.
  • 소문자 스네이크 케이스 : 테이블과 컬럼 이름은 모두 소문자로 작성하고, 단어가 여러 개 조합될 때는 언더스코어로 연결하는 스네이크 케이스를 사용하는 것이 일반적이다.
  • 명확하고 서술적인 이름 : 이름만 보고도 역할과 의미를 명확히 알 수 있도록 한다. 의미 없는 축약은 피하는 것이 좋다.
  • 일관성 있는 접두사와 접미사 : 이름에 일관성을 부여하면 가독성이 크게 향상된다.
  • 예약어 사용 금지 : ORDER, GROUP, SELECT, CREATE등 SQL에서 이미 특별한 의미를 가지는 예약어는 테이블이나 컬럼 이름으로 사용해선 안 된다.

컬럼 이름 규칙

  • 기본키(PK)는 테이블명_id로 명명한다.
  • 외래키(FK)는 참조하는 테이블의 PK 이름을 그대로 사용한다.
  • 날짜/시간 컬럼은 접미사로 용도를 명확히 한다.
    • _at : 특정 작업이 발생한 정확한 시점을 기록할 때 사용한다.
    • _datetime, _dt : 일시, 날짜와 시간 정보가 중요할 때 사용한다.
    • _date : 날짜 정보에만 사용한다. 시간 정보가 필요없거나 의미가 없는 경우에 적합하다.
    • _time : 시간 정보에만 사용한다. 날짜와 관계없이 특정 시간 정보가 필요할 때 사용한다.
  • Boolean 타입 컬럼은 is_ 또는 has_ 접두사를 사용한다.
    • 참 또는 거짓 상태를 나타내는 Boolean 컬럼은 그 의미를 명확히 하기 위해 is_has_와 같은 접두사를 붙이는 것이 좋다.
    • 이는 쿼리의 WHERE절을 읽을 때 자연스러운 문장처럼 만들어준다.
  • 컬럼은 단수 명사를 사용한다.
    • 컬럼은 테이블의 한 행이 가지는 단일 속성을 나타내므로 단수 명사를 사용해야 한다.
  • 의미 있는 축약어
    • 팀 내에서 모두가 동의하고 그 의미를 명확히 알 수 있는 축약어가 아니라면, 완전한 단어를 사용하는 것이 좋다.
  • 데이터의 성격을 명확히 표현
    • 단위나 종류 명시
    • 문맥 제공

테이블과 컬럼 변환 규칙 - 축약어

  • 모든 축약어가 나쁜 것은 아니다. 좋은 축약어는 오히려 가독성을 높이고, 널리 사용되어 의미 파악에 전혀 문제가 없다.
    • 보편성 : 누가 봐도 그 의미를 알 수 있는 업계에서 널리 통용되는 축약어여야 한다.(avg, id, max, min)
    • 비모호성 : 반드시 하나의 의미로만 해석되어야 한다.(asc, desc)
    • 일관성 및 문서화 : 특정 비즈니스 도메인에서만 통용되는 축약어를 사용해야 한다면, 반드시 프로젝트의 '데이터 사전'이나 '용어집'에 그 의미를 명확히 기록해야 한다. 그리고 프로젝트의 모든 데이터베이스에서 일관되게 사용해야 한다.
  • 나쁜 축약어는 당장의 편리함보다 훨씬 큰 비용을 장기적으로 발생시킨다.
    • 유지보수 비용 증가 : 데이터베이스를 처음 접하는 개발자는 축약어의 의미를 파악하기 위해 추가적인 시간을 소모해야 한다.
    • 버그 발생 가능성 증가 : 축약어를 오해하면 잘못된 쿼리를 작성하여 심각한 버그로 이어질 수 있다.
  • 테이블 이름으로 컨텍스트를 파악할 수 있다면 과감히 생략해도 좋다.
  • 보편적인 약어는 적극적으로 활용하면 좋다.
  • 이름이 너무 길다면, 모델링이 잘못된 것은 아닌지 의심해본다.

데이터 타입 - 문자, 숫자, PK 타입

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

문자열 타입

  • 문자열을 저장하는 타입은 크게 가변 길이와 고정 길이로 나뉜다.
  • VARCHAR(M) : 가변 길이 문자열. M은 저장할 수 있는 최대 길이를 의미한다. 실제 저장되는 데이터 길이에 따라 저장 공간의 크기가 달라진다.
    • 장점 : 공간 효율이 좋다.
    • 단점 : 길이가 변하기 때문에 데이터 수정 시 추가적인 작업이 필요할 수 있다.
    • 용도 : 이름, 제목, 주소 등 대부분의 문자열 데이터에 사용된다.
  • CHAR(M) : 고정 길이 문자열. M은 고정된 길이를 의미한다.
    • 장점 : 길이가 고정되어 있어 데이터 처리 속도가 VARCHAR보다 약간 빠를 수 있다.
    • 단점 : 공간 낭비가 심하다.
    • 용도 : 길이가 항상 고정된 데이터에 사용할 수 있다.
  • TEXT : 매우 긴 텍스트를 저장할 때 사용한다.
  • 되도록이면 일단 VARCHAR를 쓰는 것이 좋다.
  • VARCHAR 길이를 너무 길게 잡으면 메모리 사용량이 늘어날 수 있고, 너무 짧게 잡으면 데이터가 잘리는 문제가 생긴다.
  • 저장할 데이터의 평균적인 길이와 최대 길이를 고려하여 합리적으로 설정해야 한다.
    • MySQL이 쿼리를 처리하기 위해 메모리에 임시 공간을 할당할 때, VARCHAR에 설정된 최대 길이를 기준으로 삼는 경우가 많다.
    • 이 때, 메모리에 만들어지는 임시 테이블의 컬럼 크기가 원본 테이블의 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는 상태 값이나 한정된 범위의 숫자에 사용하면 공간을 매우 효율적으로 쓸 수 있다.
  • 일반적인 게시물 ID, 회원 ID 등은 INT로 충분한 경우가 많다. 하지만 양수만 저장하는 경우 UNSIGNED 옵션을 사용하면 저장 범위를 2배 늘릴 수 있다. 예를 들어, INT UNSIGNED는 0부터 약 42억까지 저장할 수 있어, 음수가 필요 없는 ID 값에 사용하기 좋다.
  • 트래픽이 많지 않은 관리자 페이지의 ID 등은 INT로도 충분하다. 하지만 사용자 주문 ID처럼 데이터가 폭발적으로 증가할 것이 확실하다면 처음부터 BIGINT를 고려하는 것이 좋다. 나중에 INT에서 BIGINT로 바꾸는 작업은 매우 힘들다.

숫자 타입 - 소수 타입

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

PK 타입 선정 : INT vs BIGINT

  • BIGINTINT에 비해 2배의 저장 공간을 사용한다.
  • 이것이 단순히 디스크 공간만 더 차지하는 문제에서 그치지 않는다. 데이터베이스에서 성능에 가장 큰 영향을 미치는 것은 I/O(디스크 입출력)다.
  • 데이터베이스는 인덱스라는 자료구조를 통해 데이터를 빠르게 찾는다. 이 때, 인덱스 정보도 결국 디스크에 저장되어 있고, 필요할 때 메모리로 읽어와야 한다.
  • PK는 가장 중요한 인덱스다. 데이터 타입의 크기가 작을수록, 한 번에 메모리에 읽어올 수 있는 인덱스 데이터 양이 많아진다. 즉, 더 적은 I/O로 원하는 데이터를 찾을 가능성이 커진다는 의미다.
  • 따라서 INT를 사용하는 것이 BIGINT를 사용하는 것보다 이론적으로 더 빠르고 효율적이다.
  • BIGINT PK를 사용하는 이유
    • 회원, 상품, 주문, 결제 등 서비스의 핵심 데이터
    • 카테고리, 상태 코드 등 내부적으로 사용하는 데이터
  • 결론은 핵심 테이블의 PK로 BIGINT사용을 권장한다.
데이터 건수 INT 저장 공간 BIGINT 저장 공간 차이
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

데이터 타입 - 날짜와 시간 타입

  • DATE : 날짜만 저장한다.
  • DATETIME : 날짜와 시간을 함께 저장한다.
  • TIME : 시간만 저장한다.
  • 회원 가입일, 주문일, 게시물 작성일 등 대부분의 경우 DATETIME을 사용한다.
  • 생년월일처럼 시간 정보가 필요 없는 경우에는 DATE를 사용한다.
  • 특별한 이유가 없다면 TIMESTAMP 대신에 DATETIME을 사용하는 것이 더 직관적이고 안전하다.
    • TIMESTAMP의 경우 시간대 변환/범위 제약이 있어 비즈니스 시각 기록은 DATETIME이 일반적으로 안전하다.
  • 생성일과 수정일은 기본이다.
    • 해당 데이터는 나중에 문제 추적이나 데이터 분석에 매우 유용하게 사용된다.
    • DEFAULT_CURRENT_TIMESTAMP : 컬럼에 기본값을 현재 시간으로 설정한다. 데이터가 처음 삽입될 때, 해당 컬럼에 값을 명시하지 않으면 데이터베이스가 자동으로 현재 시간을 기록해준다.
    • ON UPDATE CURRENT_TIMESTAMP : 해당 row의 데이터가 수정될 때마다, 자동으로 현재 시간으로 값을 갱신해준다.

비즈니스 날짜 vs 자동 생성 날짜

  • created_at은 데이터가 처음 데이터베이스에 저장된 물리적인 시간을 기록하는 용도
  • ordered_at은 주문 접수, 결제 완료 등과 같은 비즈니스 이벤트가 발생한 논리적인 시간을 기록하는 용도로 구분해서 사용하면 시스템을 더 정교하게 관리할 수 있다.

기타 타입

  • BOOLEAN : 내부적으로 TINYINT(1)로 처리된다. TRUE는 1, FALSE는 0으로 저장된다.
  • ENUM : 미리 정해진 몇 개의 문자열 값 중 하나만 저장할 수 있는 타입이다.
  • JSON : JSON 형식의 데이터를 그대로 저장할 수 있는 타입이다. 스키마가 유동적인 데이터를 저장할 때 유용하지만, 관계형 데이터베이스 장점을 제대로 활용하기 어려워 남용해선 안 된다.

역정규화

  • 정규화의 가장 큰 단점은 테이블이 잘게 분리됨에 따라 JOIN 연산이 잦아져 데이터베이스에 부하를 주어 시스템 전체 성능을 저하시키는 주범이 될 수 있다.
  • 이런 조회 성능 문제를 해결하기 위해 등장한 것이 역정규화다.
  • 역정규화란, 데이터 조회 성능 향상을 위해 의도적으로 데이터 모델의 정규화 원칙을 위반하여 데이터 중복을 허용하는 프로세스다.

정규화와 트레이드 오프 관계

구분 정규화 역정규화
목표 데이터 일관성 및 무결성 확보 데이터 조회 성능 향상
장점 데이터 중복 최소화, 쓰기 성능 유리 읽기 성능 향상(JOIN 감소)
단점 읽기 성능 저하 가능성 데이터 중복 증가, 쓰기 성능 불리, 데이터 불일치 위험 존재
  • 중복 컬럼 추가 : JOIN을 줄이기 위해 조회 시 자주 필요한 다른 테이블의 컬럼을 그대로 복사해서 가져온다.
  • 파생 컬럼 추가 : 조회 시점에 복잡한 계산이 필요하여 부하가 발생하는 경우, 그 계산 결과를 미리 컬럼에 저장해두는 방식이다.
  • 테이블 통합 및 분할 : 테이블 간의 관계, 데이터 사용 패턴을 분석해 테이블 구조를 재조정하는 방법이다.

역정규화 가이드

  • 섣부르게 적용하지 말자 : 설계 단계에서부터 역정규화를 고려하는 것은 매우 위험하다. 먼저 정규화 원칙에 따라 데이터 모델을 설계하는 것이 기본이다.
  • 데이터로 증명하라 : '느릴 것 같다'는 추측보다 실제 성능 측정을 통해 병목이 되는 쿼리를 명확히 식별해야 한다.
  • 읽기/쓰기 비율을 고려하라 : 쓰기 작업보다 읽기 작업이 압도적으로 많은 경우 적용하는 것이 효과적이다.
  • 비용을 계산하라 : 역정규화를 통해 얻는 성능상 이점과 그로 인해 발생하는 데이터 불일치 문제 해결 및 유지보수를 위한 개발 비용을 철저히 비교 분석해야 한다.