이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 데이터 무결성은 어떤 종류로 나눠 볼 수 있는가?
- 애플리케이션 검증과 DB 제약은 어떻게 역할을 나눠야 하는가?
- 제약 위반을 실무 API에서는 어떻게 처리해야 하는가?
개요
무결성은 데이터가 정확하고 일관된 상태를 유지하는 성질이다. 무결성은 사용자 입력 검증만으로 지켜지지 않는다. 배치, 운영 쿼리, 다른 서비스, 장애 복구 등 여러 경로로 데이터가 들어오기 때문이다.
DB 제약은 마지막 방어선이다.
원리
무결성 설계의 핵심은 “어느 계층에서 어떤 잘못을 막을 것인가”를 나누는 것이다. DTO 검증은 입력 형식과 사용자 경험을 담당하고, 서비스 로직은 도메인 흐름과 권한을 판단하며, DB 제약은 저장되면 안 되는 상태를 최종 저장 시점에서 거부한다.
따라서 좋은 백엔드 코드는 DB 제약에 모든 것을 떠넘기지도 않고, 서비스 코드만 믿지도 않는다. 사용자는 친절한 4xx 에러를 받고, 로그에는 어떤 constraint가 깨졌는지 남으며, 운영 데이터는 배치나 수동 정정 경로에서도 같은 불변식을 지켜야 한다.
무결성 종류
대표적으로 다음을 나눠 볼 수 있다.
- Entity integrity: 각 행이 식별 가능해야 한다.
- Referential integrity: 참조하는 대상이 존재해야 한다.
- Domain integrity: 컬럼 값이 허용 범위에 있어야 한다.
- Business integrity: 비즈니스 규칙을 만족해야 한다.
PK, FK, Not Null, Unique, Check는 앞의 세 영역을 강하게 도와준다. 비즈니스 무결성은 애플리케이션과 DB 제약이 함께 맡는다.
코드 검증과 DB 제약
회원가입에서 이메일 중복을 처리한다고 하자.
if (memberRepository.existsByEmail(email)) {
throw new DuplicateEmailException();
}
memberRepository.save(new Member(email));이 코드는 사용자에게 빠른 메시지를 주기에는 좋지만 최종 보장은 아니다. 동시 요청에서는 둘 다 exists를 통과할 수 있다.
ALTER TABLE members
ADD CONSTRAINT uq_members_email UNIQUE (email);그리고 DB 제약 위반을 잡아 도메인 예외로 변환한다.
try {
memberRepository.save(new Member(email));
} catch (DataIntegrityViolationException ex) {
throw new DuplicateEmailException();
}실무에서는 constraint name을 보고 사용자 메시지와 내부 로그를 분리한다.
WARN duplicate email blocked constraint=uq_members_email traceId=8f4a... emailHash=...API 응답은 DB 용어를 그대로 노출하지 않는다.
{
"code": "DUPLICATE_EMAIL",
"message": "이미 사용 중인 이메일입니다."
}이렇게 하면 운영자는 원인을 추적할 수 있고, 사용자는 이해 가능한 메시지를 받는다.
운영 경로
운영 DB에는 애플리케이션만 접근하지 않는다.
- migration
- backfill
- data correction script
- admin tool
- batch
- 다른 서비스
제약이 없으면 이런 경로에서 잘못된 데이터가 들어갈 수 있다.
운영 정정 스크립트도 애플리케이션 코드만큼 위험할 수 있다.
-- 정정 전 확인
SELECT email, count(*)
FROM members
GROUP BY email
HAVING count(*) > 1;
-- FK 추가 전 고아 데이터 확인
SELECT o.id
FROM orders o
LEFT JOIN members m ON m.id = o.member_id
WHERE m.id IS NULL;제약 추가는 “DDL 한 줄”이 아니라 기존 데이터 정리, lock 영향, 배포 순서, 실패 시 되돌림을 포함한 변경 작업이다.
실전 팁
- 사용자 경험을 위한 사전 검증과 DB 최종 제약을 함께 둔다.
- 제약 위반 오류는 로그에 constraint name을 남긴다.
- 비즈니스 불변식은 DB로 표현 가능한지 먼저 검토한다.
- 운영 정정 쿼리도 제약을 통과해야 한다.
- 제약 추가 전 기존 데이터가 조건을 만족하는지 검증한다.
- API 응답에는 DB constraint 이름을 그대로 노출하지 말고 도메인 에러 코드로 변환한다.
- 운영 로그에는 trace id, constraint name, 도메인 식별자 또는 안전하게 마스킹한 값을 남긴다.
- 복잡한 비즈니스 규칙은 서비스에서 판단하되, 단순하고 절대 깨지면 안 되는 하한선은 DB에 둔다.
위험 신호!
- “서비스 코드에서 막으니 DB 제약은 필요 없다”고 판단한다.
- 운영자가 직접 넣은 데이터가 애플리케이션 가정을 깨뜨린다.
- 제약 이름이 자동 생성되어 오류 분석이 어렵다.
- Unique 위반을 500 에러로 반환한다.
- 제약 추가 migration이 기존 데이터 때문에 실패한다.
- 운영 정정 스크립트가 애플리케이션 검증을 우회한다는 사실을 고려하지 않는다.
- 사용자에게는 “중복 이메일”인데 로그에는 어떤 constraint가 깨졌는지 남지 않는다.
- DTO validation을 통과했으니 DB 제약은 필요 없다고 판단한다.
확인 질문
- 애플리케이션 검증만으로 무결성을 보장하기 어려운 이유는 무엇인가?
- 동시 요청, 배치, 운영 쿼리, 다른 코드 경로를 모두 완벽히 통제하기 어렵기 때문이다.
- DB 제약 위반을 API에서 어떻게 처리해야 하는가?
- constraint name과 원인을 로깅하고, 사용자에게는 도메인에 맞는 4xx 에러로 변환하는 것이 좋다.
- 제약 추가 전 확인해야 할 것은 무엇인가?
- 기존 데이터가 제약을 만족하는지, migration이 lock과 검증 비용을 얼마나 만드는지 확인해야 한다.
- DTO 검증, 서비스 로직, DB 제약은 어떻게 역할을 나누는가?
- DTO 검증은 입력 형식과 빠른 피드백, 서비스 로직은 도메인 흐름과 권한, DB 제약은 최종 저장 시점의 불가능한 상태 차단을 맡는다.