1166 words
6 minutes
[DE Design Pattern]09-02 Constraint Enforcer

Constraints Enforcer#

01. Pattern Overview#

  • DB나 스토리지 포맷 자체에 제약조건을 선언하여 품질을 강제
  • 검증 책임을 데이터 엔지니어의 코드에서 데이터 저장소로 위임

02. 제약조건 카테고리#

Type Constraint — 특정 컬럼의 모든 값이 항상 같은 타입임을 보장.

Nullability Constraint — 컬럼이 NOT NULL인지 NULLABLE인지 정의

Value Constraint — 값의 범위나 조건을 비교 연산자로 정의. 예: event_time <= NOW() (미래 시간 불가), amount BETWEEN 0 AND 10000.

Integrity Constraint — 테이블 간 참조 무결성을 보장. Normalizer 패턴으로 모델링된 RDBMS에서 주로 사용. 예: visits 테이블의 page_id가 pages 테이블에 실제 존재해야 한다.

03. Delta Lake implmentation#

# Delta Lake: type + nullability + value constraint를 모두 선언적으로 정의
from delta.tables import DeltaTable
# 테이블 생성 시 type constraint + nullability constraint
spark.sql("""
CREATE TABLE default.visits (
visit_id STRING NOT NULL,
event_time TIMESTAMP NOT NULL,
user_id STRING,
page STRING NOT NULL
) USING delta
""")
# Value constraint 추가: event_time은 항상 과거여야 한다
spark.sql("""
ALTER TABLE default.visits
ADD CONSTRAINT event_time_not_in_future
CHECK (event_time <= current_timestamp())
""")
# 제약조건 위반 시 → DELTA_VIOLATE_CONSTRAINT_WITH_VALUES 에러 발생
# 트랜잭션 내 모든 레코드가 거부됨 (all-or-nothing)

Delta Lake는 CHECK 연산자로 value constraint를 지원하며, 위반 시 해당 트랜잭션의 전체 레코드가 거부된다.

Protobuf + protovalidate에서의 구현#

직렬화 포맷에서도 Constraints Enforcer를 적용가능. Protobuf는 기본적으로 type constraint를 지원하고, protovalidate 확장을 설치하면 value constraint까지 커버

# Protobuf 스키마 정의 (.proto 파일)
# message Visit {
# string visit_id = 1 [(buf.validate.field).string.min_len = 1];
# google.protobuf.Timestamp event_time = 2 [
# (buf.validate.field).timestamp.lt_now = true,
# (buf.validate.field).required = true
# ];
# string page = 4 [(buf.validate.field).cel = {
# message: "Page cannot end with html extension",
# expression: "this.endsWith('html') == false"
# }];
# }
# Python에서 protovalidate 사용
from protovalidate import validate
visit = Visit(
visit_id="", # min_len=1 위반
event_time=future_timestamp, # lt_now 위반
page="index.html" # cel expression 위반
)
try:
validate(visit)
except ValidationError as e:
print(f"Constraint violated: {e}")
# → invalid Visit: visit_id: value length must be at least 1...
  • producer가 레코드를 직렬화하는 시점에 검증이 일어나므로, DB에 도달하기 전에 잘못된 데이터를 차단할 수 있다

AWAP vs Constraints Enforcer#

  • AWAP는 프로그래밍 언어로 어떤 검증이든 표현할 수 있어 유연하지만, 구현과 유지보수가 엔지니어 몫이다.

  • Constraints Enforcer는 선언적이라 간단하지만, 저장소가 지원하는 제약조건 범위에 한정된다.

  • 실무에서는 둘을 조합하여 사용하는 것이 일반적 -> DB constraints로 기본 방어선을 깔고, AWAP로 복잡한 비즈니스 규칙을 추가 검증

Consequences#

All-or-nothing — DB 레벨 제약조건은 대부분 트랜잭션 기반이므로, 한 행이라도 위반하면 전체 배치가 거부. 또한 첫 번째 에러에서 멈추는 경우가 많아, 여러 문제가 있으면 수정→재시도를 반복해야 함 .

Data producer shift — 제약조건은 producer(writer) 관점에서 정의된다. 하지만 consumer마다 기대치가 다를 수 있다. 예를 들어 DB에서 nullable로 정의된 컬럼이 특정 consumer에게는 필수일 수 있어, consumer 쪽에서 추가 검증이 여전히 필요할 수 있다.

Constraints coverage — 모든 검증을 저장소 레벨로 커버할 수는 없다. 특히 table file format은 integrity constraint를 지원하지 않는 경우가 많다. 이런 경우 AWAP 패턴으로 보완해야 한다.


Concept

  • Constraints Enforcer : DB/스토리지 포맷에 선언적으로 제약조건을 정의하여 품질을 강제하는 패턴. 검증 책임을 저장소에 위임
  • Type Constraint : 컬럼의 모든 값이 동일한 데이터 타입임을 보장하는 제약. 스키마의 근간
  • Nullability Constraint : 컬럼의 NOT NULL / NULLABLE 여부를 정의하는 제약. consumer에게 결측값 가능성을 알려주는 신호 역할
  • Value Constraint : 비교 연산자 기반으로 허용 가능한 값의 범위/조건을 정의하는 제약. Delta Lake CHECK, Protobuf CEL expression
  • Integrity Constraint : 테이블 간 참조 무결성을 보장하는 제약. FK(Foreign Key)가 대표적. Normalizer 패턴과 함께 사용
  • All-or-nothing Semantics : 제약 위반 시 배치 내 전체 레코드가 거부되는 트랜잭션 동작. 부분 성공 불가
  • protovalidate : Protobuf 스키마에 value constraint를 추가하는 확장 라이브러리. 직렬화 시점에 검증 수행
  • Declarative vs Imperative 검증 : Constraints Enforcer(선언적) vs AWAP(명령적). 전자는 간단하지만 범위가 제한되고, 후자는 유연하지만 구현 비용이 높음

[DE Design Pattern]09-02 Constraint Enforcer
https://yjinheon.netlify.app/posts/02de/00-de-design-pattern/09_data_quality/09-02_constraint_enforcer/
Author
Datamind
Published at
2026-04-03
License
CC BY-NC-SA 4.0