데이터 타입과 CHECK 제약
숫자·문자·시각 타입의 도메인을 선택하고 검사로 회원 게시판의 범위와 상태 조합을 데이터베이스에 선언합니다.
타입은 저장 공간 선택을 넘어 어떤 연산과 범위를 허용할지 정합니다.
문자열에 조회수를 저장하면 ‘30회’와 ‘30’이 섞이고 날짜 문자열은 정렬과 시간대 판단이 어려워집니다.
올바른 타입 뒤에도 빈 본문, 알 수 없는 상태, 게시 상태인데 게시 시각 없음 같은 업무상 오류는 검사가 필요합니다.
앞 문서의 posts는 열 타입과 FK 골격을 가졌지만 상태 문자열과 시각 조합은 아직 자유롭습니다.
회원 상태와 게시글 제목·본문·상태·시각 규칙을 MySQL 8.4 검사로 강화합니다.
물리 타입만 사용한 검증의 한계
교재의 엄격 모드에서는 UNSIGNED 열에 음수 조회수를 넣으면 거절되지만 빈 본문과 알 수 없는 상태는 타입만으로 막지 못합니다.
VARCHAR 상태에는 철자 오류도 들어가고 PUBLISHED인데 published_at이 NULL인 행도 저장됩니다.
열 하나씩만 보면 유효해 보여도 행 전체 상태는 모순입니다.
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, published_at
) VALUES (
1, '잘못된 게시 상태', '', 'PUBLISH', 0,
'2026-07-03 09:00:00', '2026-07-03 08:00:00', NULL
);
SELECT
title,
CHAR_LENGTH(content) AS content_length,
status,
created_at,
updated_at,
(updated_at < created_at) AS updated_before_created,
published_at
FROM posts
WHERE title = '잘못된 게시 상태';검사를 추가하기 전에는 숫자 타입·문자열 길이 안에 있어 INSERT가 성공할 수 있지만 모든 업무 규칙을 어깁니다.
예상 오류 결과title | content_length | status | created_at | updated_at | updated_before_created | published_at
잘못된 게시 상태 | 0 | PUBLISH | 2026-07-03 09:00:00 | 2026-07-03 08:00:00 | 1 | NULLDBMS는 문서에만 적힌 규칙을 추론하지 않습니다.
VARCHAR와 DATETIME이 허용하는 범위와 회원 게시판이 허용할 범위는 다릅니다.
애플리케이션 열거형과 검증이 있어도 가져오기·관리 SQL·다른 서비스가 테이블에 쓸 수 있으므로 행 안에서 결정 가능한 핵심 규칙을 검사로 공유합니다.
기존 위반 행을 먼저 고치지 않으면 제약 추가에서 오류가 발생합니다.
데이터 타입과 업무 규칙
정수에는 예상 최대값과 음수 허용 여부, DECIMAL에는 정밀도·소수 자릿수, VARCHAR에는 최대 길이와 정렬 규칙, DATETIME에는 기준 시간대를 정합니다.
NULL은 값 부재를 표현하며 0·빈 문자열과 같은 뜻으로 쓰지 않습니다.
검사는 한 행 열들의 범위·조합을 불리언 식으로 선언하고 FALSE를 거절합니다.
검사 식의 알 수 없음은 통과할 수 있으므로 NOT NULL과 함께 써야 할 규칙을 구분합니다.
NOT NULL·UNIQUE·CHECK는 제약이며, DEFAULT는 생략한 값의 기본값을 정하는 별도 규칙입니다.
MySQL은 8.0.16 이후 검사를 실제 강제하며 8.4에서도 제약 조건 이름을 메타데이터에 남깁니다.
BOOLEAN은 TINYINT(1) 별칭이고 시간 타입·자동 시각 세부는 제품별 차이가 있습니다.
이 교재는 사건 시각을 UTC 의미의 DATETIME(6)로 저장하고 사용자 시간대 이름은 별도 문자열로 둡니다.
게시 상태별로 허용되는 게시 시각
| 게시 상태 | 게시 시각이 NULL | 게시 시각이 있음 |
|---|---|---|
| 초안 · DRAFT | 허용 | 거절 |
| 공개 · PUBLISHED | 거절 | 허용 |
| 숨김 · HIDDEN | 허용 | 허용 |
| 삭제 · DELETED | 허용 | 허용 |
- 초안 · DRAFT
- 게시 시각이 NULL: 허용게시 시각이 있음: 거절
- 공개 · PUBLISHED
- 게시 시각이 NULL: 거절게시 시각이 있음: 허용
- 숨김 · HIDDEN
- 게시 시각이 NULL: 허용게시 시각이 있음: 허용
- 삭제 · DELETED
- 게시 시각이 NULL: 허용게시 시각이 있음: 허용
publication_pair 식의 조합만 비교합니다. 시각이 있으면 created_at 이후 또는 같은 시각이어야 하며, 다른 CHECK도 모두 만족해야 저장됩니다.
기존 데이터 정리와 제약 추가
앞의 나쁜 실습 행을 삭제하고 회원 상태와 게시글 내용·상태·시각 조합을 추가합니다.
제약 조건 이름은 오류 로그에서 어떤 규칙이 깨졌는지 알 수 있게 업무 의미를 담습니다.
DELETE FROM posts
WHERE title = '잘못된 게시 상태';
ALTER TABLE members
ADD CONSTRAINT ck_members_status
CHECK (status IN ('ACTIVE', 'SUSPENDED', 'DELETED'));
ALTER TABLE posts
ADD CONSTRAINT ck_posts_title
CHECK (CHAR_LENGTH(TRIM(title)) BETWEEN 1 AND 80),
ADD CONSTRAINT ck_posts_content
CHECK (CHAR_LENGTH(TRIM(content)) BETWEEN 1 AND 5000),
ADD CONSTRAINT ck_posts_status
CHECK (status IN ('DRAFT', 'PUBLISHED', 'HIDDEN', 'DELETED')),
ADD CONSTRAINT ck_posts_time_order
CHECK (
updated_at >= created_at
AND (published_at IS NULL OR published_at >= created_at)
),
ADD CONSTRAINT ck_posts_publication_pair
CHECK (
(status = 'PUBLISHED' AND published_at IS NOT NULL)
OR
(status = 'DRAFT' AND published_at IS NULL)
OR
status IN ('HIDDEN', 'DELETED')
);허용 상태와 범위를 벗어난 행은 제약 조건 이름과 함께 오류가 발생하고 게시 상태·시각은 정해진 조합으로 움직입니다.
예상 결과constraint | protected rule
ck_members_status | known member lifecycle state
ck_posts_title | trimmed title length 1..80
ck_posts_content | trimmed content length 1..5000
ck_posts_status | known post lifecycle state
ck_posts_time_order | created_at <= updated/published time
ck_posts_publication_pair | public status ↔ published_atDRAFT 게시글의 view_count를 0으로 허용하고 게시 뒤 증가한 조회수를 저장합니다.
TRIM의 기본 제거 대상은 양끝의 일반 공백이며 탭·줄바꿈 전체를 비어 있는 입력으로 검사하지 않습니다. 상태 비교는 utf8mb4_0900_ai_ci를 따르므로 CHECK가 대문자 표기 자체를 강제하지도 않습니다.
복잡한 상태 전이 순서는 한 행의 CHECK만으로 모두 검증하지 않고 변경 서비스와 감사 이력이 함께 책임집니다.
메타데이터와 경계값 검증
다음 조회는 CHECK 정의와 값 분포를 확인합니다. 강제 여부는 information_schema.table_constraints의 ENFORCED 열도 별도로 조회해 확인하고, 제목 0·1·80·81자와 본문 0·1·5000·5001자 경계를 트랜잭션에서 시도합니다.
SELECT
tc.table_name,
tc.constraint_name,
tc.constraint_type,
cc.check_clause
FROM information_schema.table_constraints AS tc
LEFT JOIN information_schema.check_constraints AS cc
ON cc.constraint_schema = tc.constraint_schema
AND cc.constraint_name = tc.constraint_name
WHERE tc.constraint_schema = 'board_lab'
AND tc.table_name IN ('members', 'posts')
ORDER BY tc.table_name, tc.constraint_name;
SELECT
MIN(CHAR_LENGTH(content)) AS min_content_length,
MAX(CHAR_LENGTH(content)) AS max_content_length,
SUM(
(status = 'PUBLISHED' AND published_at IS NULL)
OR (status = 'DRAFT' AND published_at IS NOT NULL)
) AS broken_publication_state
FROM posts;모든 ck_* 정의가 보이고 본문 길이가 1..5000, broken_publication_state가 0이어야 합니다.
제약 추가 전 이 진단을 마이그레이션 선행 조건으로 실행합니다.
DATETIME은 입력한 날짜·시각을 그대로 저장하고 TIMESTAMP는 세션 시간대 변환과 더 좁은 범위를 가집니다.
어느 타입이 무조건 우월하지 않습니다.
예약된 현지 시각과 절대 사건 시각은 요구가 다릅니다.
회원 게시판은 UTC로 변환한 사건 시각을 DATETIME에 저장하고 표시 시간대는 애플리케이션 설정과 회원 환경에서 결정합니다.
DST 경계에서 현지 시각 하나가 없거나 두 번 생길 수 있으므로 변환은 시간대 라이브러리와 함께 검증합니다.
제목 1·80자는 성공하고 0·81자는 오류가 발생하는지 비교하세요.
상태 PUBLISHED와 published_at NULL, DRAFT와 NULL이 아닌 조합을 각각 시도합니다.
제약 조건을 하나씩 제거해 어떤 잘못된 행이 다시 들어가는지 관찰할 수 있지만 누적 스키마에서는 삭제하지 말고 별도 임시 테이블로 재현합니다.
오류 3819의 제약 조건 이름을 오류 응답과 연결하는 기준도 생각합니다.
대형 기존 테이블에 검사를 추가할 때 전체 행 검증과 메타데이터 잠금 비용을 평가합니다.
먼저 위반 건수를 읽기 쿼리로 확인하고 쓰기 경로를 수정한 뒤 제약을 배포합니다.
스키마 마이그레이션과 애플리케이션 배포 사이에는 구버전도 새 제약을 만족하는 호환 창이 필요합니다.
DB 오류 메시지를 클라이언트에 그대로 노출하지 않고 안정적인 도메인 오류로 변환하되 운영 로그에는 제약 조건 이름을 보존합니다.
검사의 알 수 없음 통과 때문에 CHECK (published_at >= created_at)만으로는 NULL을 막지 않습니다.
선택값이면 의도한 통과이고 필수면 NOT NULL을 추가합니다.
문자열 길이는 문자 수와 바이트 수가 다르고 인덱스 키 길이도 문자 집합 영향을 받습니다.
DECIMAL을 FLOAT로 바꾸면 정확한 비교와 집계 경계가 달라질 수 있습니다.
일반 열의 DEFAULT는 생략한 값에 적용되며 명시적 NULL의 대체값이 아닙니다. AUTO_INCREMENT 열의 NULL 입력은 번호 생성이라는 별도 규칙을 따릅니다.
여기서 완성한 회원·posts 열은 ch2 DML, ch3 필터, ch4 계산·집계가 그대로 사용합니다.
ch5는 comments, tags, post_tags 관계를 추가하고 ch8 트랜잭션은 이 검사 오류의 롤백 범위를 확인합니다.
데이터 사전에는 각 열의 타입뿐 아니라 NULL·단위·시간대·제약 조건 이유를 함께 적습니다.
타입과 제약 선택 기준
| 판단 축 | 확인할 질문 |
|---|---|
| 범위 | 예상 최소·최대와 오버플로를 계산했는가? |
| NULL | 부재와 0·빈 문자열을 구분했는가? |
| 단위 | 조회수·문자 수·시각 의미가 이름과 문서에 있는가? |
| 조합 | 상태와 관련 열이 한 행에서 모순되지 않는가? |
| 배포 | 기존 위반과 마이그레이션 잠금을 먼저 확인했는가? |
타입 이름을 관습으로 고르지 않고 허용 도메인 표를 만든 뒤 가장 좁고 충분한 타입과 제약을 선택합니다.
DB만으로 검증하기 어려운 외부 시간대·복잡한 전이는 책임 범위를 명시합니다.
연습 문제
정상 게시글, 빈 본문 게시글, 게시 시각 없는 PUBLISHED 게시글을 차례로 삽입하세요.
정상 행만 남고 두 오류의 제약 조건 이름이 예상과 같은지 기록하세요.
해설과 예시 답안
첫 행은 모든 범위와 상태 조합을 만족합니다.
둘째는 본문 길이, 셋째는 publication_pair가 거절합니다.
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, published_at
) VALUES (
1, '정상 경계 실습', '본문 길이와 상태 조합을 확인합니다.', 'PUBLISHED', 0,
'2026-07-03 09:00:00', '2026-07-03 10:00:00', '2026-07-03 10:00:00'
);
INSERT INTO posts (
author_id, title, content, status, created_at, updated_at
) VALUES (
1, '본문 오류', ' ', 'DRAFT',
'2026-07-03 11:00:00', '2026-07-03 11:00:00'
);
INSERT INTO posts (
author_id, title, content, status, created_at, updated_at, published_at
) VALUES (
1, '게시 시각 누락', '게시 상태인데 게시 시각이 없습니다.', 'PUBLISHED',
'2026-07-03 12:00:00', '2026-07-03 12:00:00', NULL
);첫 INSERT만 성공하고 오류가 각각 ck_posts_content, ck_posts_publication_pair를 가리키는지 확인합니다.
핵심 정리
- DB 타입 범위와 업무 도메인 범위는 다르므로 검사로 간격을 메웁니다.
- NULL은 부재 의미이며 0·빈 문자열과 자동으로 같지 않습니다.
- 겹치는 열의 상태 조합을 이름 있는 제약 조건으로 강제합니다.
- 기존 데이터 진단과 호환 배포를 제약 추가의 일부로 봅니다.
다음 장에서는 이 스키마를 안전하게 변경하고 실제 회원·게시글 데이터를 INSERT·UPDATE·DELETE합니다.