Post

DB 설계의 핵심: 관계 및 제약 조건

DB 설계의 핵심: 관계 및 제약 조건

관계 및 제약 조건 설계는 데이터 무결성(Integrity)을 유지하여 데이터가 항상 정확하고 일관된 상태임을 보장하는 중요한 DB 설계 원칙입니다. 이는 비즈니스 데이터를 어떻게 저장하고 연결하며 보호할지 결정하는 DB 설계의 핵심 요소 중 하나로, 잘못된 데이터 유입을 방지하고 시스템 오류를 사전에 줄이는 데 기여합니다.

관계 및 제약 조건 설계의 목적과 중요성

관계 및 제약 조건 설계의 최종 목표는 애플리케이션 코드에 오류가 발생하더라도 DB 데이터만큼은 신뢰할 수 있도록 만드는 것입니다. 서버 코드의 버그로부터 데이터를 보호하는 마지막 방어선 역할을 DB 제약이 수행하기 때문입니다.

데이터 모델링 과정에서 비즈니스 개념은 DB의 테이블, 컬럼, 관계, 제약 등으로 표현됩니다. 이는 데이터의 중복과 불일치를 방지하고 일관성을 보장하는 데 중요합니다.

DB 설계는 다음 무결성 원칙들을 보장해야 합니다:

  • 무결성 (Integrity): 데이터가 항상 정확하고 일관된 상태임을 의미합니다. 테이블 구조와 제약 조건을 통해 잘못된 값이 저장되지 않도록 보장합니다.
  • 참조 무결성: 부모-자식 테이블 관계에서 자식 레코드가 반드시 존재하는 부모 레코드를 가리키도록 강제하는 규칙입니다. 주로 외래 키(Foreign Key)로 구현됩니다.
  • 엔티티 무결성: 각 레코드가 고유한 식별자(PK)를 가져야 하며 NULL이 될 수 없다는 원칙으로, 기본 키(Primary Key) 제약으로 보장됩니다.
  • 도메인 무결성: 컬럼 값이 지정된 범위, 패턴 또는 집합을 벗어나지 않도록 하는 규칙입니다. CHECK, NOT NULL, ENUM 등이 여기에 해당합니다.
  • 멱등성 (Idempotency): 같은 요청을 여러 번 보내도 최종 결과가 한 번 호출한 것과 동일하게 유지되는 성질입니다. (user_id, idempotency_key)와 같은 복합 Unique 제약으로 보장할 수 있습니다.

주요 제약 조건 유형

데이터의 정확성과 신뢰성을 확보하기 위해 외래 키, 고유 제약, 체크 제약, NOT NULL 등 다양한 제약 조건을 적절히 조합하여 설정해야 합니다.

외래 키 (Foreign Key, FK)

두 테이블 간의 참조 무결성을 보장합니다. 부모 테이블의 기본 키에 없는 값이 자식 테이블의 외래 키에 들어오는 것을 차단하여 잘못된 데이터 유입을 막습니다.

  • 장점: 잘못된 데이터 유입을 근본적으로 방지하여 버그와 보고서 오류를 사전에 줄여줍니다.
  • 단점: 트랜잭션마다 무결성 검사를 수행하므로 대량 일괄 삽입(Bulk Insert) 시 성능이 다소 저하될 수 있습니다.
  • ON DELETE / ON UPDATE 동작: 부모 레코드 삭제 및 변경 시 자식 레코드의 동작을 정의합니다.
    • CASCADE: 부모가 삭제되면 자식 레코드도 자동으로 삭제됩니다. 과도하게 사용하면 의도치 않은 데이터 손실 위험이 있습니다.
    • SET NULL: 외래 키 값을 NULL로 변경합니다. 외래 키 컬럼이 NULL을 허용해야 합니다.
    • RESTRICT / NO ACTION: 자식 레코드가 존재하면 부모의 삭제 및 변경을 거부합니다.
  • 성능 팁: 외래 키 컬럼에는 반드시 인덱스를 생성해야 합니다. 인덱스가 없으면 부모 테이블의 변경 시 자식 테이블에서 테이블 스캔이 발생하여 성능이 저하될 수 있습니다.
  • 실무 고려사항: 실무에서는 데드락 위험, 대량 데이터 수정의 어려움 등의 이유로 물리적 FK를 적용하지 않고 애플리케이션 레벨에서 처리하는 논리적 FK만 유지하는 경우도 많습니다.

고유 제약 (Unique Constraint)

테이블 내에서 중복 레코드 생성을 방지합니다.

  • 단일 컬럼 예시: 이메일 중복 가입을 막기 위해 설정할 수 있습니다.
    1
    
    email VARCHAR(255) NOT NULL UNIQUE
    
  • 복합 컬럼 예시: API 멱등성을 보장하기 위해 결제 테이블에서 (user_id, idempotency_key)UNIQUE 제약을 적용할 수 있습니다.

체크 제약 (Check Constraint)

컬럼 값이 지정된 범위나 패턴을 벗어나면 데이터 조작을 거부합니다.

  • 숫자 범위 예시: 가격이 음수가 되지 않도록 설정합니다.
    1
    
    price DECIMAL(10, 2) CHECK (price >= 0)
    
  • 문자 패턴 예시: PostgreSQL에서 사용자 이름 패턴을 제한할 수 있습니다.
    1
    
    username VARCHAR(20) CHECK (username ~ '^[a-zA-Z0-9_]{4,20}$');
    
  • 복합 Check 제약: CHECK (start_date < end_date)와 같이 여러 컬럼 값을 동시에 검증할 수 있습니다.

NOT NULL

컬럼이 반드시 값을 가져야 함을 명시합니다. NULL 허용 여부가 로직을 복잡하게 만들 수 있으므로, 가능하면 NOT NULL로 제한하는 것이 좋습니다.

DEFAULT

컬럼 입력 시 값을 생략했을 때 자동으로 기본값을 설정하여 불필요한 코드 분기를 줄입니다.

  • 활용 예시: 생성 및 업데이트 시간을 자동으로 관리할 수 있습니다.
    1
    2
    
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    
  • 실전 팁: 타임스탬프 컬럼은 UTC(표준 기준 시간)로 저장하고, 애플리케이션 레이어에서 사용자의 지역 시간대로 변환하는 것이 일반적입니다.

지연 가능한 제약 (Deferrable Constraint)

PostgreSQL과 같은 일부 DBMS는 제약 조건 검사를 트랜잭션이 종료되는 시점까지 지연시키는 기능을 제공합니다. 이는 다단계 데이터 적재나 순환 참조 관계처럼 INSERT 순서가 문제가 될 수 있는 상황에서 유용합니다.

설계 시 고려사항

  • DB 무결성의 우선순위: 제약 조건은 서버 코드의 버그로부터 DB 데이터를 보호하는 마지막 방어선 역할을 합니다.
  • 인덱스와의 관계: FOREIGN KEYUNIQUE 제약이 걸린 컬럼에는 성능 저하를 방지하기 위해 반드시 인덱스가 필요합니다.
  • 제약 추가 전략: 대용량 테이블에 제약을 추가할 때는 Online DDL(gh-ost 등)을 활용하여 서비스 중단을 최소화해야 합니다.
  • 단계적 도입: 프로토타입 단계에서는 일부 제약을 생략하고 기능 검증 후 강화할 수 있습니다. 그러나 UNIQUE, NOT NULL과 같은 기본 제약은 초기에 설정하는 것이 좋습니다.
  • 동시성 문제: 과도한 정규화는 조인 횟수를 늘려 락 경합을 유발할 수 있으며, 반대로 과도한 비정규화는 중복 업데이트로 인한 데드락 가능성을 높일 수 있습니다.
  • 키 설계: auto_incrementUUID 같은 대리 키는 변경 가능성이 낮아 실무에서 선호됩니다. 이메일 주소와 같은 자연 키는 UNIQUE 제약으로 관리하여 안정성을 높일 수 있습니다.
This post is licensed under CC BY 4.0 by the author.