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 constraintspark.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(명령적). 전자는 간단하지만 범위가 제한되고, 후자는 유연하지만 구현 비용이 높음