이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 제약 조건 위반은 API에서 어떻게 응답해야 하는가?
  • Constraint name은 왜 운영 디버깅에 중요한가?
  • DB 제약을 추가하거나 변경할 때 어떤 배포 절차가 필요한가?

개요

제약 조건은 데이터 정합성을 지키지만, 위반 시 예외를 만든다. 좋은 백엔드 코드는 이 예외를 그대로 500으로 흘리지 않고 도메인 의미로 변환한다. 또한 운영자가 어떤 제약이 깨졌는지 알 수 있게 이름과 로그를 관리한다.

원리

제약 조건 운영의 원리는 “DB가 막은 오류를 백엔드가 도메인 언어로 번역하는 것”이다. DB는 uq_members_email이 깨졌다고 말하고, API는 사용자에게 “이미 사용 중인 이메일”이라고 말해야 한다. 동시에 운영 로그에는 constraint name, trace id, 안전하게 마스킹한 식별자가 남아야 재현과 정정이 가능하다.

Spring의 DataIntegrityViolationException은 유용한 추상화지만, 모든 DB와 driver에서 같은 세부 정보를 주지는 않는다. PostgreSQL, MySQL, Hibernate, JDBC driver에 따라 SQLState, constraint name 추출 방식이 달라질 수 있으므로, 중요한 제약은 통합 테스트로 실제 예외 변환까지 확인해야 한다.

예외 변환

이메일 Unique 위반을 예로 들자.

ALTER TABLE members
ADD CONSTRAINT uq_members_email UNIQUE (email);

Spring에서는 DB 제약 위반이 DataIntegrityViolationException 계열로 변환될 수 있다.

try {
    memberRepository.save(new Member(email));
} catch (DataIntegrityViolationException ex) {
    throw new DuplicateEmailException();
}

실무에서는 constraint name을 보고 어떤 도메인 예외로 바꿀지 판단하는 계층을 둘 수 있다.

private RuntimeException translate(DataIntegrityViolationException ex) {
    String constraint = constraintNameExtractor.extract(ex);
    if ("uq_members_email".equals(constraint)) {
        return new DuplicateEmailException();
    }
    if ("fk_orders_member".equals(constraint)) {
        return new InvalidMemberReferenceException();
    }
    return ex;
}

이 코드는 개념 예시다. 실제 구현에서는 driver별 예외 구조, Hibernate wrapping, 개인정보 로그 정책을 함께 고려한다.

API 응답

중복 이메일은 서버 오류가 아니라 클라이언트 요청 충돌이다.

{
  "code": "DUPLICATE_EMAIL",
  "message": "이미 사용 중인 이메일입니다."
}

HTTP 상태는 보통 409 Conflict 또는 정책에 따라 400을 사용할 수 있다. 중요한 것은 DB 예외를 그대로 노출하지 않는 것이다.

FK 위반처럼 클라이언트가 존재하지 않는 리소스를 참조한 경우는 400, 404, 409 중 도메인 API 정책에 맞춰 정한다. 핵심은 상태 코드보다 “같은 비즈니스 오류가 항상 같은 에러 코드로 응답되는가”다.

Constraint 이름

자동 생성된 이름은 분석이 어렵다.

ERROR: duplicate key value violates unique constraint "uk_8q9..."

명시적 이름은 로그를 읽기 쉽게 한다.

CONSTRAINT uq_members_email UNIQUE (email)

장애 중 constraint name만 봐도 어떤 규칙이 깨졌는지 빠르게 알 수 있다.

제약 추가 절차

기존 테이블에 제약을 추가할 때는 기존 데이터 검증이 필요하다.

SELECT email, count(*)
FROM members
GROUP BY email
HAVING count(*) > 1;

중복 데이터가 있으면 Unique 제약 추가가 실패한다. 먼저 정리 정책을 정해야 한다.

대형 테이블에서는 제약 검증과 lock 영향도 확인한다. PostgreSQL은 NOT VALID 후 validation 같은 전략을 사용할 수 있는 경우가 있다.

ALTER TABLE orders
ADD CONSTRAINT fk_orders_member
FOREIGN KEY (member_id) REFERENCES members(id)
NOT VALID;
 
ALTER TABLE orders
VALIDATE CONSTRAINT fk_orders_member;

이런 전략은 DBMS와 제약 종류에 따라 가능 여부와 lock 동작이 다르다. MySQL에서는 온라인 DDL 지원 범위, 알고리즘, lock 옵션을 별도로 확인해야 한다. “공식 문서에 되는 문법”과 “우리 버전/엔진/트래픽에서 안전한 작업”은 다를 수 있다.

실전 팁

  • 제약 이름은 도메인 의미를 드러내게 짓는다.
  • 제약 위반은 도메인 예외와 4xx 응답으로 변환한다.
  • 운영 로그에는 constraint name과 trace id를 남긴다.
  • 제약 추가 전 기존 데이터 위반 여부를 검사한다.
  • 대형 테이블 제약 추가는 lock과 검증 비용을 DBA와 확인한다.
  • 실제 사용하는 DB와 driver 조합에서 constraint name이 추출되는지 통합 테스트로 확인한다.
  • 제약 추가 migration에는 사전 검증 SQL, 정정 SQL, 배포 시간대, 실패 시 대응을 함께 둔다.
  • 사용자 응답에는 DB constraint name을 노출하지 않고, 내부 로그에는 도메인 에러 코드와 constraint name을 함께 남긴다.

위험 신호!

  • Unique 위반이 500으로 응답된다.
  • DB 에러 메시지가 사용자에게 그대로 노출된다.
  • constraint name이 자동 생성되어 원인 파악이 어렵다.
  • 기존 데이터 검증 없이 제약 추가 migration을 배포한다.
  • 제약 위반을 모두 같은 에러 코드로 처리한다.
  • 개발 환경 H2나 테스트 DB에서는 통과했는데 운영 PostgreSQL/MySQL 예외 변환은 검증하지 않았다.
  • constraint name 추출 실패 시 모든 제약 위반이 500으로 떨어진다.
  • 제약 추가 DDL이 실패했을 때 중복 데이터 정리 기준이 없다.

확인 질문

  • 제약 위반을 도메인 예외로 변환해야 하는 이유는 무엇인가?
    • DB 내부 오류가 아니라 사용자 요청이나 비즈니스 규칙 위반으로 해석해야 할 경우가 많기 때문이다.
  • Constraint name을 명시적으로 짓는 이유는 무엇인가?
    • 로그와 예외에서 어떤 규칙이 깨졌는지 빠르게 파악하기 위해서다.
  • 기존 테이블에 Unique 제약을 추가하기 전에 해야 할 일은 무엇인가?
    • 기존 데이터 중복 여부를 확인하고 정리한 뒤 lock과 검증 비용을 고려해 migration해야 한다.
  • 제약 위반 예외 변환을 통합 테스트해야 하는 이유는 무엇인가?
    • Spring 추상화 아래의 DB, JDBC driver, Hibernate wrapping 방식에 따라 constraint name과 SQLState를 얻는 방식이 달라질 수 있기 때문이다.

참고 문서