전통적인 데이터 레이크는 트랜잭션을 지원하지 않기 때문에, 데이터 정합성을 유지하기 어려운 단점이 있다. 반면, Delta Lake는 트랜잭션을 지원하는 데이터 레이크로, ACID(Atomicity, Consistency, Isolation, Durability) 속성을 보장하며 데이터 무결성을 보다 효과적으로 유지할 수 있다.
그러나 Delta Lake에서 주장하는 트랜잭션과 무결성이 RDBMS에서의 트랜잭션과 동일한 수준으로 보장되는지에 대한 의문이 들 수 있다 (우선 내가 그랬다). 이 글에서는 Delta Lake의 트랜잭션 동작 방식과 RDBMS와의 차이점, 그리고 Delta Lake에서 데이터 정합성을 유지하기 위한 전략을 정리해보려 한다.
델타 테이블에서의 트랜잭션
Delta Table에서 트랜잭션은 Delta Table 을 이루는 파일들을 변경하는 모든 작업을 의미한다. DeltaLake 트랜잭션 로그에 새로운 메타데이터 항목이 추가되는 작업을 의미한다. 또한 기존 테이블을 이루는 데이터를 재배열 하는 작업 (ex. Z-Ordering) 이나 작은 파일을 압축하는 작업도 트랜잭션에 포함 된다.
예를 들어 아래와 같이 델타 테이블을 생성하는 트랜잭션이 일어난다고 한다면, 테이블을 저장하는 hdfs 경로의 하위인 '_delta_log' 폴더 하위에 트랜잭션 로그가 새롭게 기록된다.
df = spark.createDataFrame([(1, "cat"), (2, "dog"), (3, "snake")], schema=["num", "animal"])
df.write.format("delta").saveAsTable("/tmp/my-delta-table")
tmp/my-delta-table
├── 0-fea2de92-861a-423e-9708-a9e91dafb27b-0.parquet
└── _delta_log
└── 00000000000000000000.json
데이터의 write 작업이 일어나는 예시로, 테이블에서 'cat' 에 해당하는 row 를 삭제하는 트랜잭션을 수행해본다.
dt = DeltaTable("tmp/my-delta-table")
dt.delete("animal = 'cat'")
DELETE 연산이 일어난 후, 새롭게 기록되는 트랜잭션 로그의 내용은 아래와 같다. 트랜잭션이 커밋된 정보에 대해서 알 수 있다.
{
"add": {
"path": "part-00001-90312b96-b487-4a8f-9edc-1b9b3963f136-c000.snappy.parquet",
"partitionValues": {},
"size": 858,
"modificationTime": 1705070631953,
"dataChange": true,
"stats": "{\"numRecords\":2,\"minValues\":{\"num\":2,\"animal\":\"dog\"},\"maxValues\":{\"num\":3,\"animal\":\"snake\"},\"nullCount\":{\"num\":0,\"animal\":0}}",
"tags": null,
"deletionVector": null,
"baseRowId": null,
"defaultRowCommitVersion": null,
"clusteringProvider": null
}
}
{
"remove": {
"path": "0-fea2de92-861a-423e-9708-a9e91dafb27b-0.parquet",
"dataChange": true,
"deletionTimestamp": 1705070631953,
"extendedFileMetadata": true,
"partitionValues": {},
"size": 895
}
}
{
"commitInfo": {
"timestamp": 1705070631953,
"operation": "DELETE",
"operationParameters": {
"predicate": "animal = 'cat'"
},
"readVersion": 0,
"operationMetrics": {
"execution_time_ms": 8013,
"num_added_files": 1,
"num_copied_rows": 2,
"num_deleted_rows": 1,
"num_removed_files": 1,
"rewrite_time_ms": 2,
"scan_time_ms": 5601
},
"clientVersion": "delta-rs.0.17.0"
}
}
Delta Lake에서는 새로운 트랜잭션을 수행할 때 먼저 기존에 존재하는 메타데이터(metadata) 기록을 읽고, 이후 '실제 데이터'에 해당하는 Parquet 파일을 로드한다. 현재 실행 중인 트랜잭션을 처리한 후, 그 결과를 새로운 Parquet 파일로 저장하고, 트랜잭션 로그(Delta Log)를 JSON 파일 형식으로 기록한다.
Delta Lake는 트랜잭션을 지원하므로, 실행된 트랜잭션이 충돌을 일으키지 않는지 반드시 확인한다. 새로운 Parquet 파일을 저장한 후, 충돌이 발생하지 않은 경우에만 해당 변경 사항을 반영하고 트랜잭션 로그를 기록한다.
Delta Lake는 다중 버전 동시성 제어 (non locking MVCC [multi version concurrency control]) 방식을 사용하여 트랜잭션을 구현한다. 데이터 쓰기 작업을 수행할 때, 낙관적 동시성 제어를 적용하여, 먼저 새로운 데이터를 기록한 후, 충돌이 일어나면 해당 트랜잭션을 단순히 포기하는 방식으로 동작한다. MySQL, Oracle 같은 주요 RDBMS 에서 트랜잭션 시작 시점에 잠금을 획득하고, 충돌없이 트랜잭션을 보장하는 잠금 기반 동시성 제어 (Lock Based Concurrency Control) 방식으로 동작하는 것과 차이가 있다.
DeltaLake 가 MVCC 방식을 선택한 이유
왜 Delta Lake 는 전통적인 잠금 기반 동시성 제어보다 다소 허술해 보이는 MVCC 방식을 채택했을까 ?
MVCC 방식의 가장 큰 장점은 읽기 작업과 쓰기 작업 간의 충돌이 발생하지 않는다는 점이다. 읽기 작업은 기존의 데이터 버전을 참조하고, 쓰기 작업은 새로운 버전을 생성하기 때문에 동일한 데이터에 대한 읽기 및 쓰기 작업이 동시에 수행될 수 있다. 즉, 잠금 없이 대량의 데이터를 조회할 수 있다. 반면, 전통적인 잠금 기반 동시성 제어에서는 여러 사용자가 동시에 대량의 데이터를 읽고 쓸 때 병목이 발생할 가능성이 크다.
MVCC 방식에서 여러 사용자가 동시에 데이터 쓰기 작업을 할 때, 각 트랜잭션이 독립적으로 새로운 버전을 생성한다. 이로 인해, 트랜 잭션 충돌이 발생할 가능성이 줄어들고 동시 쓰기 성능이 향상된다. 반면, 잠금 기반 방식에서는 동시에 쓰기 작업을 하는 경우 트랜잭션이 대기해야 하므로 성능 저하가 일어날 수 있다. 따라서, DeltaLake 는 데이터 레이크 환경에서 대량의 데이터를 동시에 읽고, 쓸 수 있도록 하기 위해 MVCC 방식을 채택하게 된 것이다.
트랜잭션 격리 수준
트랜잭션 격리 수준은 격리 수준이 높은순서대로 Serializable, Repeatable Read, Read Committed, Read Uncommitted 로 나뉘게 된다.
Serializable | 가장 엄격한 격리 수준. 트랜잭션을 직렬화된 순서대로 실행시킨다. 트랜잭션이 순차처리되기 때문에 동시 처리 기능이 떨어진다. |
Repeatable Read | MVCC 방식로 구현되어 한 트랜잭션 내에서 동일한 결과를 보장하지만, 새로운 레코드가 추가되는 경우 부정합(Phantom Read)이 발생할 수 있다. |
Read Committed | 커밋된 데이터만 조회할 수 있다. 반복 읽기를 수행하는 경우 다른 트랜잭션의 커밋 여부에 따라 조회 결과가 달라질 수 있는 문제가 발생한다. |
Read Uncommitted | 커밋하지 않은 데이터에도 접근할 수 있는 격리수준이다. 다른 트랜잭션이 커밋 혹은 롤백되지 않아도 즉시 보이게 된다. 이를 Dirty Read라고 한다. |
DeltaLake 에서의 트랜 잭션 격리 수준을 정리하면 아래와 같다.
- 트랜잭션이 끝나지 않은 데이터는 보이지 않음 (Read Committed 수준 보장)
- 읽는 도중 데이터가 변경되지 않음 (Repeatable Read 수준 보장)
- 멀티버전 컨트롤(MVCC)로 직렬화된 트랜잭션을 일부 보장 (Serializable 수준 일부 지원)
또한 DeltaLake가 지원하는 트랜잭션은 오직 하나의 테이블에 대해서만 보장된다. 멀티 테이블 트랜잭션은 지원하지 않는다.
DeltaLake 에서의 무결성
무결성이란, 데이터의 정확성, 일관성, 유효성이 유지되는 것을 의미한다. 즉 데이터가 손상되지 않고 오류 없이 저장되는 것을 보장하는 원칙이다.
무결성의 주요 유형에는 아래와 같은 원칙들이 있다.
- 개체 무결성 : 각 테이블의 기본키는 고유해야 한다. 기본키는 고유한 값을 가져야 하며, null 값을 허용하지 않는다.
- 참조 무결성 : RDBMS에서 참조 관계에 있는 두 테이블의 데이터가 항상 일관된 값을 갖도록 유지하는 것을 의미한다. 왜래 키는 반드시 참조하는 테이블의 기본 키 값과 일치해야 한다.
- 도메인 무결성 : 특정 열(column)에 저장될 수 있는 데이터 값의 범위를 제한한다.
- 고유성 무결성 : 특정 컬럼에 저장된 값이 중복되지 않도록 보장한다.
DeltaLake에서의 무결성 보장 방법은 RDBMS에 비해서 느슨하다. 예를 들기 위해, airport_code를 PRIMARY KEY로 하는 airports 테이블을 생성했다.
그리고 3개의 row 를 insert 하는 연산을 수행한다.
LAX 라는 key 값이 존재함에도 불구하고 아래의 row 가 insert 된 것을 확인할 수 있다.
DeltaLake 에서 데이터 정합성 보장하기
그렇다면, DeltaLake 에서는 어떤식으로 정합성을 보장해야 할까? 아래와 같은 방법들이 고려될 수 있다.
- CHECK CONSTRAINT 사용: Delta Lake 3.0 이상에서 CHECK 제약 조건을 활용하여 기본키와 왜래키 지정, 컬럼 조건을 지정할 수 있다. 그렇지만 참고로 아직 정식 지원은 아니아서 Only PRIMARY KEY and FOREIGN KEY constraints are currently supported. 라는 오류가 발생중이다.
- MERGE 연산 활용: MERGE INTO를 활용해 왜래키 참조 무결성을 구현할 수 있다. 예를 들어서, 직접 spark 코드로 참조하는 테이블의 PK와 현재 테이블의 FK를 확인후, 정합성에 일치하지 않는 row를 삭제하는 비즈니스 로직을 개발해야 정합성을 유지할 수 있다.
-- Delta Lake에서는 FOREIGN KEY가 강제 적용되지 않으므로, MERGE INTO를 활용한 정합성 유지 방법을 고려할 수 있음.
MERGE INTO demo.flights f
USING demo.airports a
ON f.dep_airport = a.airport_code
WHEN NOT MATCHED THEN DELETE;
결론
Delta Lake는 기존의 데이터 레이크에서 부족했던 트랜잭션과 데이터 정합성 문제를 해결하기 위해 등장했다. 하지만, RDBMS처럼 강력한 참조 무결성(FK)이나 도메인 무결성을 자동으로 보장하지는 않는다. 결과적으로, Delta Lake는 데이터 레이크 환경에서 대량의 데이터를 안정적으로 관리할 수 있는 선택지이지만, RDBMS와 동일한 수준의 데이터 무결성을 기대하기 어렵기 때문에 비즈니스 로직을 통한 정합성 유지 전략을 함께 고려해야 한다는 점을 기억해야 한다.
Reference
https://delta-io.github.io/delta-rs/how-delta-lake-works/delta-lake-acid-transactions/#conclusion
'Data Engineering > Databricks & Delta Lake' 카테고리의 다른 글
[DeltaLake] (1) - 대표적인 오픈 테이블 포맷인 Delta Lake 와 Ice Berg 비교하기 (0) | 2025.02.15 |
---|---|
MLOps 파이프라인 설계 및 MLflow 활용 방법 (0) | 2024.11.24 |
[Databricks] Delta Live Table 로 파이프라인 개발 & 데이터 퀄리티 모니터링하기 (0) | 2024.04.14 |
[Databricks] 데이터브릭스의 Unity Catalog 기능에 대해 살펴보기 (0) | 2024.03.31 |
[TIL] Delta Table에 upsert 하기 (0) | 2024.02.14 |