데이터 타입과 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·검사·기본값은 표준 제약 개념입니다.
MySQL은 8.0.16 이후 검사를 실제 강제하며 8.4에서도 제약 조건 이름을 메타데이터에 남깁니다.
BOOLEAN은 TINYINT(1) 별칭이고 시간 타입·자동 시각 세부는 제품별 차이가 있습니다.
이 교재는 사건 시각을 UTC 의미의 DATETIME(6)로 저장하고 사용자 시간대 이름은 별도 문자열로 둡니다.
기존 데이터 정리와 제약 추가
앞의 나쁜 실습 행을 삭제하고 회원 상태와 게시글 내용·상태·시각 조합을 추가합니다. 제약 조건 이름은 오류 로그에서 어떤 규칙이 깨졌는지 알 수 있게 업무 의미를 담습니다.
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으로 허용하고 게시 뒤 증가한 조회수를 저장합니다.
HIDDEN과 DELETED는 게시 이력이 있으면 published_at을 유지하고, 게시 전에 전환됐다면 NULL일 수 있습니다.
복잡한 상태 전이 순서는 한 행의 CHECK만으로 모두 검증하지 않고 변경 서비스와 감사 이력이 함께 책임집니다.
메타데이터와 경계값 검증
이름만 생성된 것이 아니라 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로 바꾸면 정확한 비교와 집계 경계가 달라질 수 있습니다.
기본값은 값 생략 시 적용되고 명시적 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합니다.