PostgreSQL 제약 우선: AI 엣지 케이스 버그를 초기에 막기
코드와 테스트에 앞서 PostgreSQL의 NOT NULL, CHECK, UNIQUE, FOREIGN KEY 제약을 활용해 AI가 생성한 앱을 더 안전하게 배포하세요.

왜 AI가 작성한 코드가 여전히 실제 데이터에서 깨지는가
AI가 작성한 코드는 종종 정상 흐름(happy path)을 잘 처리해서 올바르게 보입니다. 하지만 실제 애플리케이션은 중간의 지저분한 상황에서 실패합니다: 폼이 null 대신 빈 문자열을 보내거나, 백그라운드 작업이 재시도되어 동일한 기록을 두 번 만들거나, 삭제가 부모 행을 지우고 자식 레코드를 고아로 남기기도 합니다. 이런 문제들은 희귀한 버그가 아닙니다. 필수 필드가 비어 보이거나, “고유” 값이 중복되거나, 아무것도 가리키지 않는 고아 행으로 나타납니다.
이런 문제들은 코드 리뷰나 기본 테스트를 통과하기 쉽습니다. 그 이유는 단순합니다: 리뷰어는 의도를 읽지, 모든 엣지 케이스를 확인하지 않습니다. 테스트는 보통 몇 가지 전형적인 예만 커버하고, 수 주간의 실제 사용자 행동, CSV에서의 임포트, 불안정한 네트워크 재시도, 동시 요청 등은 다루지 않습니다. 어시스턴트가 생성한 코드라면 공백 제거, 범위 검증, 레이스 컨디션 방지 같은 작은 하지만 치명적인 검사들을 놓치기 쉽습니다.
“제약 우선, 코드 차선” 접근은 데이터베이스에 불가역적 규칙을 넣어 어떤 코드 경로가 쓰기를 시도하든 잘못된 데이터가 저장되지 않게 하는 뜻입니다. 사용자에게는 더 나은 오류 메시지를 주기 위해 애플리케이션에서 입력을 검증하는 것이 여전히 필요하지만, 데이터베이스가 최종 진실을 강제해야 합니다. PostgreSQL 제약은 바로 이런 상황에서 빛을 발합니다. 전체 카테고리의 실수를 방지해 줍니다.
간단한 예를 들어보면: 작은 CRM이 있다고 가정합시다. AI가 생성한 임포트 스크립트가 연락처를 만듭니다. 어떤 행은 이메일이 ""(빈 문자열)이고, 두 행은 서로 다른 대소문자로 같은 이메일을 반복하며, 한 연락처는 계정이 다른 프로세스에서 삭제돼 존재하지 않는 account_id를 참조합니다. 제약이 없다면 이런 데이터는 프로덕션에 그대로 쌓여 나중에 리포트를 망가뜨립니다.
적절한 데이터베이스 규칙이 있으면 그런 쓰기는 즉시 실패합니다. 발생 지점에 가깝게 차단됩니다. 필수 필드는 빠져 있을 수 없고, 재시도 중 중복이 생길 수 없으며, 관계는 삭제되었거나 존재하지 않는 레코드를 가리킬 수 없고, 값은 허용 범위를 벗어날 수 없습니다.
제약이 모든 버그를 막지는 못합니다. 혼란스러운 UI나 잘못된 할인 계산, 느린 쿼리를 고쳐주진 않습니다. 하지만 잘못된 데이터가 조용히 쌓이는 것을 막아 주며, 그게 종종 “AI가 만든 엣지 케이스 버그”가 비용으로 이어지는 지점입니다.
왜 데이터베이스를 최후의 방어선으로 삼아야 하는가
애플리케이션은 흔히 하나의 코드베이스가 하나의 사용자와만 통신하는 구조가 아닙니다. 일반적인 제품에는 웹 UI, 모바일 앱, 관리자 화면, 백그라운드 작업, CSV 임포트, 그리고 때로는 서드파티 통합이 포함됩니다. 각 경로는 데이터를 생성하거나 변경할 수 있습니다. 모든 경로가 동일한 규칙을 기억해야 한다면, 어느 하나는 반드시 잊게 됩니다.
데이터베이스는 그 모든 경로가 공유하는 장소입니다. 데이터베이스를 최종 심사관으로 다루면 규칙이 자동으로 모두에게 적용됩니다. PostgreSQL 제약은 “우리는 이게 항상 참이라고 가정한다”를 “이것이 반드시 참이어야 하며, 아니면 쓰기가 실패한다”로 바꿔줍니다.
AI가 생성한 코드는 이 점을 더 중요하게 만듭니다. 모델은 React UI에서 폼 유효성 검사를 추가할 수 있지만 백그라운드 작업의 구석 케이스를 놓칠 수 있습니다. 또는 정상 경로의 데이터는 잘 처리하더라도 실제 고객이 예기치 않은 값을 입력하면 무너질 수 있습니다. 제약은 잘못된 데이터가 들어가려는 순간을 잡아냅니다. 수 주 후 이상한 리포트를 디버깅할 때가 아니라요.
제약을 건너뛰면 잘못된 데이터는 종종 소리 없이 들어갑니다. 저장은 성공하고 앱은 넘어가며, 문제는 지원 티켓, 청구 불일치, 또는 아무도 신뢰하지 않는 대시보드로 나타납니다. 이력을 고치는 데에는 비용이 많이 듭니다. 왜냐하면 과거 전체를 고치는 것이기 때문입니다.
잘못된 데이터는 보통 일상적인 상황을 통해 들어옵니다: 새 클라이언트 앱 버전이 필드를 누락 대신 빈 값으로 보내거나, 재시도가 중복을 만들거나, 관리자 편집이 UI 검사를 우회하거나, 임포트 파일 포맷이 일관되지 않거나, 두 사용자가 관련 레코드를 동시에 업데이트하는 경우 등입니다.
유용한 사고 모델은 경계에서만 데이터를 받아들이는 것입니다. 실제로 그 경계에는 데이터베이스가 포함되어야 합니다. 데이터베이스는 모든 쓰기를 보기 때문입니다.
NOT NULL: 필수 데이터 누락을 조기에 차단하기
NOT NULL은 가장 단순한 PostgreSQL 제약이며 놀랄 만큼 많은 버그를 예방합니다. 행이 의미를 가지려면 값이 반드시 있어야 한다면 데이터베이스에 이를 강제하세요.
NOT NULL은 식별자, 필수 이름, 타임스탬프에 보통 적합합니다. 유효한 레코드를 생성할 수 없다면 비어 있게 하지 마세요. 작은 CRM에서 소유자나 생성 시간이 없는 리드는 "부분 리드"가 아니라, 나중에 이상한 행동을 일으키는 손상된 데이터입니다.
NULL은 AI가 생성한 코드에서 더 자주 들어옵니다. 왜냐하면 "옵션" 경로를 무심코 만들기 쉽기 때문입니다. 폼 필드가 UI에서 선택적일 수 있고, API가 누락된 키를 허용할 수 있으며, 생성 함수의 한 분기에서 값을 할당하지 않을 수 있습니다. 모든 것이 컴파일되고 정상 흐름의 테스트는 통과합니다. 그다음 실제 사용자가 빈 셀을 가진 CSV를 임포트하거나 모바일 클라이언트가 다른 페이로드를 보내면 NULL이 데이터베이스에 들어오게 됩니다.
좋은 패턴은 NOT NULL을 시스템이 소유하는 필드의 합리적인 기본값과 결합하는 것입니다:
created_at TIMESTAMP NOT NULL DEFAULT now()status TEXT NOT NULL DEFAULT 'new'is_active BOOLEAN NOT NULL DEFAULT true
기본값이 항상 정답은 아닙니다. email이나 company_name처럼 사용자가 제공하는 필드를 NOT NULL을 만족시키려고 기본값으로 채우지 마세요. 빈 문자열은 NULL보다 "더 유효"하지 않습니다. 문제를 숨기는 것일 뿐입니다.
확실하지 않을 때는 그 값이 정말로 "알 수 없음(unknown)"인지, 아니면 다른 상태를 나타내는지 결정하세요. "아직 제공되지 않음"이 의미가 있다면 모든 곳에서 NULL을 허용하는 대신 별도의 상태 컬럼을 고려하세요. 예를 들어 phone은 nullable로 두고 phone_status를 missing, requested, verified 같은 값으로 관리하면 의미를 코드 전반에 걸쳐 일관되게 유지할 수 있습니다.
CHECK: 비즈니스 규칙을 데이터 근처에 인코딩하기
CHECK 제약은 테이블이 지키는 약속입니다: 모든 행은 항상 어떤 규칙을 만족해야 합니다. 이는 코드 상에서는 괜찮아 보이지만 실제로는 말이 안 되는 레코드가 조용히 생성되는 것을 막는 가장 쉬운 방법 중 하나입니다.
CHECK 제약은 같은 행의 값만으로 판별할 수 있는 규칙에 가장 적합합니다: 숫자 범위, 허용값, 컬럼 간의 간단한 관계 등입니다.
-- 1) Totals should never be negative
ALTER TABLE invoices
ADD CONSTRAINT invoices_total_nonnegative
CHECK (total_cents >= 0);
-- 2) Enum-like allowed values without adding a custom type
ALTER TABLE tickets
ADD CONSTRAINT tickets_status_allowed
CHECK (status IN ('new', 'open', 'waiting', 'closed'));
-- 3) Date order rules
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_date_order
CHECK (end_date IS NULL OR end_date >= start_date);
좋은 CHECK 제약은 한눈에 읽을 수 있어야 합니다. 그것을 데이터에 대한 문서처럼 취급하세요. 짧은 표현, 명확한 제약 이름, 예측 가능한 패턴을 선호하세요.
CHECK가 모든 상황에 맞는 도구는 아닙니다. 규칙이 다른 행을 조회하거나 집계 데이터, 또는 테이블 간 비교를 필요로 하면(예: "계정이 플랜 한도를 초과할 수 없다" 등) 그 로직은 애플리케이션 코드, 트리거, 또는 제어된 백그라운드 작업에 두세요.
UNIQUE: 나중에 후회할 중복을 방지하기
UNIQUE 규칙은 단순합니다: 데이터베이스는 제약된 컬럼(또는 여러 컬럼의 조합)에 동일한 값을 가진 두 행을 저장하지 않습니다. 이는 "생성" 경로가 두 번 실행되거나 재시도가 발생하거나 두 사용자가 동시에 같은 것을 제출하는 경우에 발생하는 많은 버그를 제거합니다.
UNIQUE는 정의한 정확한 값에 대해 중복이 없음을 보장합니다. 하지만 값이 존재하는지(NOT NULL), 형식을 따르는지(CHECK), 또는 대소문자·공백·구두점의 차이를 어떻게 처리할지는(동등성의 정의) 보장하지 않습니다. 그 부분은 직접 정의해야 합니다.
일반적으로 고유성을 원할 만한 곳은 사용자 테이블의 이메일, 외부 시스템에서 온 external_id, 또는 (account_id, name)처럼 계정 내에서 고유해야 하는 이름 등입니다.
한 가지 주의점: NULL과 UNIQUE. PostgreSQL에서 NULL은 "알 수 없음"으로 취급되어 UNIQUE 제약 아래에서 여러 NULL 값이 허용됩니다. "값이 존재해야 하고 고유해야 한다"면 UNIQUE와 NOT NULL을 같이 사용하세요.
사용자 대면 식별자에 대한 실용적 패턴은 대소문자 구분 없는 고유성입니다. 사람들은 "[email protected]"을 입력하고 나중에 "[email protected]"을 입력하며 같은 것으로 기대합니다.
-- Case-insensitive unique email
CREATE UNIQUE INDEX users_email_unique_ci
ON users (lower(email));
-- Unique contact name per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_name_unique UNIQUE (account_id, name);
사용자 관점에서 "중복"이 무엇을 의미하는지(대소문자, 공백, 계정 단위 vs 전역)를 정의하고, 한 번만 규칙을 인코딩하세요. 그러면 모든 코드 경로가 같은 규칙을 따릅니다.
FOREIGN KEY: 관계를 일관되게 유지하기
FOREIGN KEY는 "이 행은 저쪽에 실제 행을 가리켜야 한다"고 말합니다. 이 제약이 없으면 코드가 고아 레코드를 조용히 생성해 나중에 앱을 깨뜨릴 수 있습니다. 예를 들어 삭제된 고객을 가리키는 노트, 또는 존재하지 않는 사용자 ID를 가리키는 인보이스 등입니다.
외래 키는 삭제와 생성이 근접하게 일어날 때 가장 중요합니다: 삭제와 생성, 타임아웃 후 재시도, 또는 오래된 데이터로 백그라운드 작업이 실행되는 경우 등. 데이터베이스는 일관성을 강제하는 데 있어 모든 앱 경로가 규칙을 기억하게 하는 것보다 더 낫습니다.
적절한 ON DELETE 동작 선택하기
ON DELETE 옵션은 관계의 실제 의미와 일치해야 합니다. 질문하세요: "부모가 사라지면 자식은 여전히 존재해야 하는가?"
RESTRICT(또는NO ACTION): 자식이 있으면 부모 삭제를 차단합니다.CASCADE: 부모를 삭제하면 자식도 삭제합니다.SET NULL: 자식은 남기되 링크만 제거합니다.
CASCADE는 조심해서 사용하세요. 올바를 수 있지만, 버그나 관리자 실수로 부모 레코드를 삭제하면 예상보다 많은 데이터를 지울 수 있습니다.
멀티테넌트 스키마: 소유권을 명시적으로 모델링하기
멀티테넌트 앱에서 외래 키는 단순한 정확성 이상의 역할을 합니다. 계정 간 누수를 방지합니다. 일반적인 패턴은 테넌트 소유 테이블마다 account_id를 포함하고 관계를 통해 소유권을 묶는 것입니다.
CREATE TABLE contacts (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
PRIMARY KEY (account_id, id)
);
CREATE TABLE notes (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
contact_id bigint NOT NULL,
body text NOT NULL,
PRIMARY KEY (account_id, id),
FOREIGN KEY (account_id, contact_id)
REFERENCES contacts (account_id, id)
ON DELETE RESTRICT
);
이렇게 하면 스키마에서 "누가 무엇을 소유하는가"를 강제합니다: 앱 코드(또는 LLM이 생성한 쿼리)가 시도하더라도 노트는 다른 계정의 연락처를 가리킬 수 없습니다.
단계별: 프로덕션을 깨뜨리지 않고 제약 추가하기
먼저 불변성 목록을 짧게 작성하세요: 항상 참이어야 하는 사실들입니다. 평이하게 작성하세요. "모든 연락처는 이메일이 필요하다." "상태는 몇 가지 허용된 값 중 하나여야 한다." "인보이스는 실제 고객에 속해야 한다." 이런 규칙들이 데이터베이스가 매번 강제하길 원하는 규칙입니다.
변경은 작은 마이그레이션으로 롤아웃해 프로덕션을 놀라게 하지 마세요:
- 먼저 비파괴적으로 새 컬럼이나 규칙을 추가하세요.
- 기존 행을 배치로 백필(backfill)하세요.
- 잘못된 데이터를 정리(중복 제거, 잘못된 값 수정)하거나 검토를 위해 격리(quarantine)하세요.
- 규칙을 강제하세요(
NOT NULL,UNIQUE,CHECK,FOREIGN KEY). - 오류를 명확하게 처리하도록 앱 동작을 조정하세요.
문제는 기존의 잘못된 데이터입니다. 이를 계획하세요. 중복의 경우 우승자 행을 선택해 나머지를 병합하고 소규모 감사 노트를 남기세요. 필수 필드가 누락된 경우 안전한 기본값을 선택할 수 있을 때만 사용하고, 그렇지 않으면 격리하세요. 깨진 관계의 경우 자식 행을 올바른 부모로 재할당하거나 잘못된 행을 제거하세요.
각 마이그레이션 후에는 실패해야 할 몇 가지 쓰기를 직접 검증하세요: 필수 값이 없는 행 삽입, 중복 키 삽입, 범위를 벗어난 값 삽입, 없는 부모 행을 참조하는 삽입 등. 실패하는 쓰기는 유용한 신호입니다. 그것은 앱이 "최선의 노력" 동작에 조용히 의존하던 지점을 보여줍니다.
현실적인 예: 데이터가 깔끔한 작은 CRM
작은 CRM을 떠올려 보세요: 고객(당신의 SaaS 고객), 그들이 일하는 회사들, 그 회사의 연락처, 회사에 연결된 딜들.
이런 앱은 채팅 도구로 빠르게 생성되기 쉽습니다. 데모에서는 괜찮아 보이지만 실제 데이터는 빠르게 지저분해집니다. 초기에 흔히 나타나는 두 가지 버그는 연락처 중복(미세하게 다른 방식으로 같은 이메일이 입력됨)과 회사 없이 생성된 딜(어떤 경로에서 company_id를 설정하지 않음)입니다. 또 다른 고전적인 예는 리팩터나 파싱 실수로 음수 딜 값이 생기는 경우입니다.
해결책은 더 많은 if 문이 아닙니다. 잘 선택한 몇 가지 제약으로 잘못된 데이터가 저장되는 것을 불가능하게 만드는 것입니다.
CRM을 깨끗하게 유지하는 제약들
-- Contacts: prevent duplicates per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_email_uniq UNIQUE (account_id, email);
-- Deals: require a company and keep the relationship valid
ALTER TABLE deals
ALTER COLUMN company_id SET NOT NULL,
ADD CONSTRAINT deals_company_fk
FOREIGN KEY (company_id) REFERENCES companies(id);
-- Deals: deal value cannot be negative
ALTER TABLE deals
ADD CONSTRAINT deals_value_nonneg CHECK (deal_value >= 0);
-- A few obvious required fields
ALTER TABLE companies
ALTER COLUMN name SET NOT NULL;
ALTER TABLE contacts
ALTER COLUMN email SET NOT NULL;
이것은 단지 엄격해지기 위한 것이 아닙니다. 모호한 기대를 데이터베이스가 항상 강제할 수 있는 규칙으로 바꾸는 것입니다. 어떤 앱 부분이 쓰기를 하든 동일하게 적용됩니다.
제약이 적용된 뒤 앱에서 바뀌는 것들
이 제약들이 적용되면 앱은 더 단순해집니다. 사후에 중복을 감지하려는 방어적 검사들을 많이 제거할 수 있습니다. 실패는 더 명확하고 조치 가능해집니다(예: "이 계정에 이미 존재하는 이메일입니다" 같은 메시지). 생성된 API 경로가 필드를 빼먹거나 값을 잘못 처리하면 쓰기가 즉시 실패하여 데이터베이스를 조용히 손상시키는 것을 막습니다.
제약이 골칫거리가 되는 흔한 실수들
제약은 비즈니스가 실제로 작동하는 방식과 일치할 때 가장 잘 작동합니다. 대부분의 골칫거리는 순간적으로는 "안전해 보이는" 규칙을 추가했지만 나중에 놀라움이 되는 경우입니다.
흔한 함정은 ON DELETE CASCADE를 모든 곳에 사용하는 것입니다. 깔끔해 보이지만 누군가 부모를 삭제하면 시스템 절반이 삭제되는 상황을 초래할 수 있습니다. CASCADE는 진정으로 소유된 데이터(단독으로 존재해서는 안 되는 임시 항목 등)에 적절할 수 있지만 고객, 인보이스, 티켓 같은 중요한 레코드에 대해서는 위험합니다. 확실하지 않다면 RESTRICT를 선호하고 삭제를 의도적으로 처리하세요.
또 다른 문제는 너무 좁은 CHECK 규칙을 쓰는 것입니다. "status는 'new', 'won', 'lost'여야 한다"는 말은 나중에 "paused"나 "archived"가 필요해질 때 문제가 됩니다. 좋은 CHECK는 일시적 UI 선택이 아니라 안정적인 진실을 묘사해야 합니다. "amount >= 0"은 오래 견딥니다. "country in (...)"는 자주 바뀝니다.
팀이 이미 생성된 코드를 운영 중일 때 제약을 추가하면 반복적으로 나타나는 문제들:
CASCADE를 정리 도구로 취급했다가 의도보다 많은 데이터를 삭제함.- 유효한 미래 케이스를 막는 지나치게 엄격한 CHECK.
- NULL이 관련된 경우 UNIQUE가 중복을 막지 못한다고 가정함.
- 이미 규칙을 위반하는 행들을 고치지 않고 규칙만 바꿈.
성능에 관해: PostgreSQL은 UNIQUE에 대해 자동으로 인덱스를 생성하지만, 외래 키의 참조 컬럼은 자동으로 인덱스가 생기지 않습니다. 해당 컬럼에 인덱스가 없으면 부모의 업데이트나 삭제 시 Postgres가 자식 테이블을 스캔해야 해서 느려질 수 있습니다.
규칙을 강화하기 전에 실패할 기존 행들을 찾아 고칠 것인지 격리할 것인지 결정하고, 단계적으로 변경을 롤아웃하세요.
빠른 체크리스트와 다음 단계
배포 전에 테이블별로 5분만 투자해 항상 참이어야 할 것을 적어보세요. 평범한 영어로 말할 수 있으면 보통 제약으로 강제할 수 있습니다.
각 테이블에 대해 다음 질문을 하세요:
- 무엇이 절대 NULL이어서는 안 되는가?
- 무엇이 절대 중복되어서는 안 되는가?
- 무엇이 음수거나 범위를 벗어나면 안 되는가?
- 무엇이 부모 없이 존재해서는 안 되는가(고아 행 금지)?
- 무엇이 단순 규칙을 항상 따라야 하는가(허용된 상태, 시작이 종료보다 이전 등)?
채팅 기반 빌드 도구를 사용한다면 이러한 불변성을 데이터에 대한 수용 기준(acceptance criteria)으로 다루세요. 예: "딜 금액은 음수일 수 없다", "연락처 이메일은 워크스페이스별로 고유해야 한다", "작업은 실제 연락처를 참조해야 한다" 등. 규칙을 명확히 할수록 우연한 엣지 케이스의 여지가 줄어듭니다.
Koder.ai (koder.ai)는 플래닝 모드, 스냅샷과 롤백, 소스 코드 내보내기 같은 기능을 포함하여 제약을 점진적으로 강화하면서 스키마 변경을 안전하게 반복하기 쉽게 만듭니다.
실무에서 효과적인 간단한 롤아웃 패턴: 가치가 큰 테이블 한 개(사용자, 주문, 인보이스, 연락처 등)를 골라 1~2개의 제약(NOT NULL, UNIQUE인 경우가 많음)을 추가하고, 실패하는 쓰기를 고친 뒤 반복하세요. 한 번에 큰 위험을 감수하는 것보다 규칙을 점차 강화하는 것이 더 낫습니다.
자주 묻는 질문
데이터 규칙을 앱에만 두지 않고 PostgreSQL에 넣어야 하는 이유는 무엇인가요?
항상 지켜져야 하는 규칙은 PostgreSQL에 두고, 모든 앱 경로가 그 규칙을 따르게 하세요. UI와 API 검증은 친절한 메시지를 제공할 수 있고, 제약 조건은 가져오기, 작업, 재시도, 관리자 도구에서 발생하는 잘못된 쓰기를 막습니다.
언제 NOT NULL을 사용해야 하나요?
소유자 ID, 이름, 생성 시간처럼 값이 없으면 행의 의미가 없는 경우 NOT NULL을 사용하세요. 빈 문자열을 누락된 사용자 입력의 대체값으로 쓰지 마세요. 오류를 숨기게 됩니다.
CHECK 제약 조건은 무엇에 유용한가요?
CHECK은 amount >= 0, 허용된 상태값, 시작일 이후의 종료일처럼 한 행 안에서 적용되는 규칙에 사용하세요. 향후 제품 변경으로 유효한 데이터가 오류가 되지 않도록 규칙은 짧고 안정적으로 유지하세요.
UNIQUE는 누락된 값도 막아 주나요?
중복이 실제 문제를 일으키는 경우, 예를 들어 외부 ID가 반복되거나 하나의 계정에 같은 이메일을 가진 연락처가 두 개 생기는 경우 UNIQUE를 추가하세요. 모든 레코드에 그 값이 반드시 있어야 한다면 NOT NULL과 함께 사용하세요. PostgreSQL은 표준 고유 제약 조건에서 여러 NULL을 허용하기 때문입니다.
외래 키는 고아 레코드를 어떻게 방지하나요?
외래 키는 존재하지 않는 부모를 가리키는 자식 행을 데이터베이스가 거부하게 합니다. 유효한 회사가 없는 거래나 삭제된 고객에 연결된 메모처럼 고아 레코드가 생기는 것을 막습니다.
ON DELETE CASCADE를 어디에나 사용해야 하나요?
부모를 삭제하려면 신중한 결정이 필요할 때는 RESTRICT부터 사용하세요. 임시 초안 항목처럼 부모 없이는 자식 레코드가 정말 가치가 없을 때만 CASCADE를 선택하세요.
대소문자만 다른 중복 이메일을 어떻게 막을 수 있나요?
사용자가 대소문자가 달라도 같은 이메일로 인식한다고 기대한다면 lower(email)처럼 정규화된 값에 고유 인덱스를 사용하세요. 앱에서도 입력값의 공백을 제거하고 정규화하면 더 명확한 오류 메시지를 보여 줄 수 있습니다.
프로덕션 데이터베이스에 제약 조건을 안전하게 추가하려면 어떻게 해야 하나요?
먼저 새 규칙을 위반하는 행을 찾으세요. 배치로 수정, 병합 또는 격리한 다음 제약 조건을 추가하고, 쓰기 실패 시 앱이 유용한 오류를 보여 주도록 업데이트하세요.
외래 키에는 인덱스가 필요한가요?
고유 제약 조건은 자체 인덱스를 만들지만, 외래 키는 자식 열에 자동으로 인덱스를 만들지 않습니다. 테이블이 크거나 부모 행이 자주 업데이트 또는 삭제된다면 참조하는 열에 인덱스를 추가하세요.
어떤 제약 조건부터 추가해야 하나요?
돈, 고객 또는 신원에 영향을 주는 테이블 하나부터 시작하세요. 필수 소유자, 계정별 고유 이메일, 0 이상 금액처럼 가치가 분명한 규칙을 한두 개 추가한 뒤 실패해야 하는 쓰기를 테스트하세요.