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절을 읽을 때 자연스러운 문장처럼 만들어준다.
- 참 또는 거짓 상태를 나타내는 Boolean 컬럼은 그 의미를 명확히 하기 위해
- 컬럼은 단수 명사를 사용한다.
- 컬럼은 테이블의 한 행이 가지는 단일 속성을 나타내므로 단수 명사를 사용해야 한다.
- 의미 있는 축약어
- 팀 내에서 모두가 동의하고 그 의미를 명확히 알 수 있는 축약어가 아니라면, 완전한 단어를 사용하는 것이 좋다.
- 데이터의 성격을 명확히 표현
- 단위나 종류 명시
- 문맥 제공
테이블과 컬럼 변환 규칙 - 축약어
- 모든 축약어가 나쁜 것은 아니다. 좋은 축약어는 오히려 가독성을 높이고, 널리 사용되어 의미 파악에 전혀 문제가 없다.
- 보편성 : 누가 봐도 그 의미를 알 수 있는 업계에서 널리 통용되는 축약어여야 한다.(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
BIGINT는INT에 비해 2배의 저장 공간을 사용한다.- 이것이 단순히 디스크 공간만 더 차지하는 문제에서 그치지 않는다. 데이터베이스에서 성능에 가장 큰 영향을 미치는 것은 I/O(디스크 입출력)다.
- 데이터베이스는 인덱스라는 자료구조를 통해 데이터를 빠르게 찾는다. 이 때, 인덱스 정보도 결국 디스크에 저장되어 있고, 필요할 때 메모리로 읽어와야 한다.
- PK는 가장 중요한 인덱스다. 데이터 타입의 크기가 작을수록, 한 번에 메모리에 읽어올 수 있는 인덱스 데이터 양이 많아진다. 즉, 더 적은 I/O로 원하는 데이터를 찾을 가능성이 커진다는 의미다.
- 따라서
INT를 사용하는 것이BIGINT를 사용하는 것보다 이론적으로 더 빠르고 효율적이다. BIGINTPK를 사용하는 이유- 회원, 상품, 주문, 결제 등 서비스의 핵심 데이터
- 카테고리, 상태 코드 등 내부적으로 사용하는 데이터
- 결론은 핵심 테이블의 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을 줄이기 위해 조회 시 자주 필요한 다른 테이블의 컬럼을 그대로 복사해서 가져온다. - 파생 컬럼 추가 : 조회 시점에 복잡한 계산이 필요하여 부하가 발생하는 경우, 그 계산 결과를 미리 컬럼에 저장해두는 방식이다.
- 테이블 통합 및 분할 : 테이블 간의 관계, 데이터 사용 패턴을 분석해 테이블 구조를 재조정하는 방법이다.
역정규화 가이드
- 섣부르게 적용하지 말자 : 설계 단계에서부터 역정규화를 고려하는 것은 매우 위험하다. 먼저 정규화 원칙에 따라 데이터 모델을 설계하는 것이 기본이다.
- 데이터로 증명하라 : '느릴 것 같다'는 추측보다 실제 성능 측정을 통해 병목이 되는 쿼리를 명확히 식별해야 한다.
- 읽기/쓰기 비율을 고려하라 : 쓰기 작업보다 읽기 작업이 압도적으로 많은 경우 적용하는 것이 효과적이다.
- 비용을 계산하라 : 역정규화를 통해 얻는 성능상 이점과 그로 인해 발생하는 데이터 불일치 문제 해결 및 유지보수를 위한 개발 비용을 철저히 비교 분석해야 한다.