제약 조건 오류 진단
NOT NULL·UNIQUE·FK·검사 오류를 제약 조건과 입력 책임으로 분류하고 올바른 값·순서로 복구합니다.
제약 오류는 DB가 귀찮게 막는 예외가 아니라 데이터 규칙이 작동한 증거입니다.
모든 오류를 ‘저장 오류’ 하나로 묶으면 사용자 입력, 중복 요청, 부모 순서, 상태 전이 중 무엇을 고쳐야 하는지 알 수 없습니다.
오류 번호와 제약 조건 이름을 업무 원인에 연결합니다.
회원과 posts에 있는 NOT NULL, 이메일 UNIQUE, 작성자 FK, 본문·상태 검사를 차례로 위반합니다.
이 예제의 InnoDB·IGNORE 없는 제약 실패 문장은 행을 남기지 않습니다. AUTO_INCREMENT에 빈 번호가 생기는 것은 가능하며, 각 원인을 고친 정상 게시글로 마지막 일치 검사를 수행합니다.
제약 오류의 잘못된 처리
필수 이메일 없음, 중복 이메일, 없는 회원, 공백 본문을 같은 일시 장애처럼 다시 실행합니다.
입력과 참조 데이터가 그대로인 재시도는 같은 제약 위반을 반복할 수 있습니다. UNIQUE 충돌을 무조건 성공으로 무시하면 다른 사용자의 행을 자기 요청 결과로 오인할 수 있습니다.
-- NOT NULL / required field
INSERT INTO members (email, password_hash, name)
VALUES (NULL, '{demo}hash', '필수값 누락');
-- UNIQUE / duplicate business key
INSERT INTO members (email, password_hash, name)
VALUES ('[email protected]', '{demo}other-hash', '중복 사용자');
-- FOREIGN KEY / missing parent
INSERT INTO posts (
author_id, title, content, status, created_at, updated_at, request_key
) VALUES (
999999, '고아 게시글', '존재하지 않는 작성자를 참조합니다.', 'DRAFT',
'2026-07-07 00:00:00', '2026-07-07 00:00:00', 'orphan-001'
);
-- CHECK / value outside domain
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, request_key
) VALUES (
1, '본문 오류', ' ', 'DRAFT',
0, '2026-07-07 01:00:00', '2026-07-07 01:00:00', 'invalid-content-001'
);예상 오류는 앞 장의 회원 1과 [email protected]이 있고, 회원 999999와 새 예시 요청 키는 없다는 전제입니다. 예제를 반복하거나 다른 데이터를 추가했다면 먼저 위반하는 규칙이 달라질 수 있습니다.
다음 문장을 클라이언트가 계속 실행했는지와 트랜잭션 상태는 실행 방식에 따라 확인합니다.
예상 오류 결과1048 | Column 'email' cannot be null
1062 | Duplicate entry '[email protected]' for key 'uq_members_email'
1452 | Cannot add or update a child row: fk_posts_author
3819 | Check constraint 'ck_posts_content' is violated오류 번호만 저장하지 않고 제약 조건 이름, 요청 키, 안전한 입력 요약을 로그에 남기며 개인정보 원문은 제한합니다.
오류 번호와 제약 이름으로 수정할 대상을 찾는다
| 오류와 의미 | 먼저 확인할 입력 | 다음 행동 |
|---|---|---|
| 1048 · 필수값 | NULL인 필수 열 | 필수 값을 제공하거나 누락 원인을 수정합니다. |
| 1062 · 중복 키 | 충돌한 UNIQUE와 기존 행 | 같은 업무 요청인지 비교한 뒤 충돌 정책을 적용합니다. |
| 1452 · 부모 없음 | 자식 FK 값과 참조 대상 | 부모의 존재·저장 순서·잘못된 키를 확인합니다. |
| 3819 · 검사 위반 | 실패한 CHECK 이름과 행의 값 | 해당 범위나 열 조합을 수정합니다. |
- 1048 · 필수값
- 먼저 확인할 입력:
NULL인 필수 열다음 행동: 필수 값을 제공하거나 누락 원인을 수정합니다. - 1062 · 중복 키
- 먼저 확인할 입력: 충돌한 UNIQUE와 기존 행다음 행동: 같은 업무 요청인지 비교한 뒤 충돌 정책을 적용합니다.
- 1452 · 부모 없음
- 먼저 확인할 입력: 자식 FK 값과 참조 대상다음 행동: 부모의 존재·저장 순서·잘못된 키를 확인합니다.
- 3819 · 검사 위반
- 먼저 확인할 입력: 실패한 CHECK 이름과 행의 값다음 행동: 해당 범위나 열 조합을 수정합니다.
입력과 참조 데이터가 그대로라면 맹목적인 재시도로 해결되지 않습니다. 여러 규칙을 동시에 어긴 행에서는 먼저 보고된 제약부터 확인합니다.
제약 조건별 무결성
하나의 오류를 없애려고 제약을 끄지 않고 데이터 생성 순서와 입력을 고칩니다.
부모를 먼저 만들고 자식을 넣으며, 중복 요청이면 기존 행이 같은 업무 요청인지 멱등성 키로 확인합니다.
DB 오류는 애플리케이션의 안정적인 도메인 오류로 변환합니다.
제약 개념은 표준이지만 MySQL 오류 번호·메시지 형식은 제품 규칙입니다.
애플리케이션은 메시지 문자열 파싱보다 공급자 코드·SQLSTATE·제약 조건 메타데이터를 이용하고 버전 업그레이드에서 변화를 검증합니다.
INSERT IGNORE나 ON DUPLICATE KEY UPDATE는 오류를 없애는 만능 도구가 아니라 명시적인 충돌 정책이 있을 때 선택합니다.
부모와 자식의 저장 순서
기존 회원을 이메일로 찾고 허용 범위와 상태 조합을 만족하는 게시글을 저장합니다.
중복 회원을 새로 만들지 않고 현재 행을 참조하며 오류를 숨기는 IGNORE는 사용하지 않습니다.
SELECT id, email, status
FROM members
WHERE email = '[email protected]'
AND status = 'ACTIVE';
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, published_at, request_key
) VALUES (
(SELECT id FROM members WHERE email = '[email protected]'),
'제약 조건 복습', '제약 조건이 지키는 범위를 게시글로 정리합니다.',
'PUBLISHED', 50,
'2026-07-07 02:00:00', '2026-07-07 02:50:00',
'2026-07-07 02:50:00', 'post-min-constraint-001'
);
SELECT id, author_id, title, view_count, status, version
FROM posts
WHERE title = '제약 조건 복습';예시 출력은 참조 회원이 존재하고 다른 필수·범위·상태 제약도 충족한 경우입니다. 사전 SELECT의 ACTIVE 조건은 INSERT의 이메일 서브쿼리에 들어 있지 않으므로, 조회 결과가 0행이어도 뒤 INSERT가 자동으로 중단되지는 않습니다.
예상 결과parent rows | 1
inserted rows | 1
view_count | 50
status/time | PUBLISHED + non-NULL published_at
constraint errors | 0SELECT 후 INSERT 사이 부모 상태가 바뀔 수 있으므로 애플리케이션 트랜잭션과 권한 검사를 함께 설계합니다.
FK는 부모 존재만 보장하고 활성 상태까지 강제하지 않습니다.
활성 사용자만 기록할 수 있다는 규칙은 서비스 트랜잭션 또는 더 복잡한 모델이 책임집니다.
DB 제약이 보장하는 것과 보장하지 않는 것을 오류 처리에서 구분합니다.
제약 위반 검사
정상 삽입 뒤 현재 데이터에서 고아, 범위, 상태 조합, 이메일 중복이 없는지 진단합니다.
제약이 이미 막더라도 마이그레이션·검사 우회·복구 데이터를 확인하는 기준 쿼리로 남깁니다.
SELECT
(SELECT COUNT(*)
FROM members
GROUP BY email
HAVING COUNT(*) > 1
LIMIT 1) AS duplicate_email_group,
(SELECT COUNT(*)
FROM posts AS p
LEFT JOIN members AS m ON m.id = p.author_id
WHERE m.id IS NULL) AS orphan_posts,
(SELECT COUNT(*) FROM posts
WHERE CHAR_LENGTH(TRIM(title)) NOT BETWEEN 1 AND 80
OR CHAR_LENGTH(TRIM(content)) NOT BETWEEN 1 AND 5000) AS range_violations,
(SELECT COUNT(*) FROM posts
WHERE (status = 'PUBLISHED' AND published_at IS NULL)
OR (status = 'DRAFT' AND published_at IS NOT NULL))
AS state_pair_violations;duplicate_email_group은 중복 그룹이 없으면 스칼라 NULL입니다. 중복이 있다면 LIMIT 1로 고른 한 그룹의 행 수이며 중복 그룹 총수는 아닙니다. 나머지 세 진단 값은 0을 기대합니다.
진단 쿼리 자체의 NULL 의미도 결과 규칙에 적습니다.
DB 제약과 애플리케이션 검증은 중복이 아니라 다른 시간과 사용자 경험을 담당합니다.
애플리케이션은 빠르고 친절한 오류를 주지만 경쟁 요청 사이의 UNIQUE는 DB만 최종 판단할 수 있습니다.
검사 오류를 HTTP 400, UNIQUE 충돌을 409처럼 변환할 수 있지만 모든 중복이 같은 도메인 의미는 아닙니다.
내부 버그와 사용자 수정 가능 입력을 분리합니다.
네 오류를 한 구문씩 실행하고 오류 번호·SQLSTATE·제약 조건 이름을 표에 기록하세요.
부모를 먼저 추가하면 1452가 사라지는지, 공백 본문을 실제 문장으로 바꾸면 3819가 사라지는지 확인합니다.
중복 이메일은 이름을 바꿔도 계속 중복임을 보고 키가 어느 열인지 설명합니다.
PUBLISHED 상태에 게시 시각을 넣은 뒤 다른 검사가 연쇄적으로 나타나는지도 순서대로 해결합니다.
오류율 지표는 제약 조건별 범위 제한 태그로 집계하고 이메일·id 같은 고유값을 지표 표시명으로 넣지 않습니다.
갑자기 1452가 늘면 배포 순서·이벤트 지연·잘못된 스키마 연결을 조사하고, 1062가 늘면 재시도·멱등성 문제를 봅니다.
제약을 긴급히 비활성화하기 전에 쓰기 중단·원인 수정·격리 테이블·복구 계획을 검토합니다.
UNIQUE는 NULL 허용 열에서 여러 NULL을 허용할 수 있고 정렬 규칙에 따라 대소문자·악센트가 같은 값으로 비교될 수 있습니다.
FK는 같은 트랜잭션 안에서 부모를 먼저 넣으면 커밋 전에도 참조할 수 있지만 MySQL은 일반적으로 지연된 제약 조건을 제공하지 않습니다.
검사의 NULL 알 수 없음 통과, 다중-행 구문 한 행 오류, 트리거가 만든 추가 오류도 구분합니다.
이 오류 분류는 ch5 FK 마이그레이션, ch8 트랜잭션 재시도, ch13 API 오류 규칙에 그대로 이어집니다.
제약 조건 이름을 안정적인 스키마 규칙으로 관리하면 애플리케이션과 운영 대시보드가 같은 원인을 말할 수 있습니다.
뒤의 가져오기 장에서는 거부된 행을 입력 배치 ID와 함께 격리해 수정 후 재처리합니다.
제약 오류 대응 기준
| 판단 축 | 확인할 질문 |
|---|---|
| 종류 | NOT NULL·UNIQUE·FK·검사 중 어느 축인가? |
| 지속성 | 같은 입력의 재시도로 해결되는 일시 오류인가? |
| 책임 | 사용자 입력·쓰기 순서·애플리케이션 버그 중 누가 고치는가? |
| 노출 | 클라이언트에는 안정적인 도메인 오류, 로그에는 제약 조건 증거가 남는가? |
| 복구 | 제약을 끄지 않고 올바른 데이터·순서로 재처리하는가? |
오류 메시지를 지우고 재시도하지 않습니다.
제약 조건 이름과 위반 키를 찾아 수정 가능한 입력인지 시스템 결함인지 분류한 뒤 각기 다른 후속 행동을 선택합니다.
연습 문제
아래 의도를 만족하는 게시글을 스스로 넣으세요: ch2-2 본문에서 저장한 [email protected], 조회수 50회, PUBLISHED 상태, 고유한 request_key.
먼저 published_at을 빼서 오류를 확인한 뒤 올바르게 수정하세요.
해설과 예시 답안
부모는 이메일 UNIQUE 서브쿼리로 한 ID를 얻고 PUBLISHED 상태에는 NULL이 아닌 published_at을 함께 제공합니다.
-- 실패: ck_posts_publication_pair
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, published_at, request_key
) VALUES (
(SELECT id FROM members WHERE email = '[email protected]'),
'제약 진단 연습', '게시 시각 조합을 확인합니다.', 'PUBLISHED', 50,
'2026-07-08 01:00:00', '2026-07-08 01:00:00', NULL, 'post-sora-check-001'
);
-- 개선
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, published_at, request_key
) VALUES (
(SELECT id FROM members WHERE email = '[email protected]'),
'제약 진단 연습', '게시 시각 조합을 확인합니다.', 'PUBLISHED', 50,
'2026-07-08 01:00:00', '2026-07-08 01:50:00',
'2026-07-08 01:50:00', 'post-sora-check-001'
);첫 문장은 3819와 ck_posts_publication_pair 이름으로 오류가 발생하고 둘째만 저장되는지, 같은 제목이 UNIQUE가 아니므로 다른 게시글 제목과 겹칠 수 있음을 확인합니다.
핵심 정리
- NOT NULL·UNIQUE·FK·검사는 서로 다른 무결성 질문을 지킵니다.
- 영구 입력 오류를 같은 입력으로 재시도하거나 제약을 끄지 않습니다.
- DB 오류를 제약 조건 증거와 안정적인 도메인 오류로 연결합니다.
- 애플리케이션 검증과 DB 제약은 사용자 경험과 경쟁 안전을 나누어 맡습니다.
운영 중 필수 열을 추가하는 절차는 ch2-1에서 다시 확인할 수 있습니다. 다음 3장에서는 준비한 누적 데이터를 조회합니다.