변경 데이터 캡처
고친 사람 github-actions[bot]
변경 데이터 캡처는 데이터베이스에 생긴 변경을 한 건씩 뽑아내 다른 시스템으로 흘려보냅니다. 전체를 다시 읽어 견주는 대신 바뀐 것만 일어난 순서대로 건넵니다. 검색 색인이나 분석용 저장소처럼 원본을 따라가야 하는 곳이 이 흐름을 받아 씁니다.
쉽고 빠른 이해
변경 데이터 캡처는 데이터베이스에서 방금 바뀐 것만 뽑아 다른 시스템에 넘깁니다. 「주문 8871번의 상태가 결제됨으로 바뀌었다」가 넘어가는 한 건입니다.
이게 없으면 원본을 통째로 다시 읽어 견주거나, 애플리케이션이 두 곳에 각각 써야 합니다. 앞쪽은 비용이 큽니다. 뒤쪽은 한쪽만 성공했을 때 어긋납니다.
- 데이터베이스가 남겨 둔 변경 기록을 따라 읽습니다
- 변경 한 건을 어디까지 읽었는지 표시와 함께 내보냅니다
- 받는 쪽이 자기 저장소에 같은 변경을 적용합니다
대가는 받는 쪽이 늘 조금 뒤처진다는 것입니다. 원본이 바뀐 순간과 받는 쪽에 닿는 순간 사이가 벌어집니다. 그 사이에는 둘이 서로 다른 값을 보여 줍니다. 쓰자마자 같은 값을 읽어야 하는 곳에는 안 맞습니다.
상세
창고에 무엇이 남았는지 알고 싶을 때, 문을 열고 선반을 처음부터 세는 방법이 있습니다. 물건이 들어오고 나갈 때마다 쪽지를 한 장 붙여 두고 그 쪽지만 모아 보는 방법도 있습니다. 변경 데이터 캡처는 뒤쪽입니다.
변경 데이터 캡처는 Change Data Capture 를 옮긴 말입니다. 줄여서 CDC 라고도 합니다.
데이터베이스는 자기가 한 변경을 순서대로 파일에 적어 둡니다. 이 파일이 변경 기록입니다.
적어 두는 까닭은 둘입니다. 장애가 났을 때 이 파일을 되짚어 끊긴 지점부터 되살립니다. 복제본에 같은 변경을 보낼 때도 이 파일을 씁니다. 제품마다 트랜잭션 로그 · 미리 쓰기 로그 · binlog 처럼 이름이 다르지만 하는 일은 같습니다.
변경을 받아 자기 저장소에 적용하는 쪽을 받는 쪽이라고 부릅니다. 검색 색인 · 분석용 저장소 · 캐시처럼 원본을 따라가야 하는 저장소가 여기 섭니다.
그 기록을 읽어 변경을 한 건씩 내보내는 프로그램이 캡처기입니다. 캡처기는 원본과 받는 쪽 사이에 서서 읽은 것을 넘깁니다. 어디까지 읽었는지를 기억하는 것도 캡처기가 하는 일입니다.
변경 한 건의 다섯 칸
넘어가는 변경 한 건은 받는 쪽이 그것만 보고 같은 일을 자기 저장소에 다시 해낼 수 있어야 합니다. 받는 쪽은 원본 데이터베이스를 들여다볼 수 없고 건네받은 한 건만 봅니다.
칸 하나가 빠지면 그만큼 다시 해낼 수 없는 것이 생깁니다.
| 칸 | 담는 것 | 빠지면 |
|---|---|---|
| 표와 키 | 어느 표의 어느 행인가 | 무엇을 고쳐야 할지 모릅니다 |
| 연산 | 넣었나 고쳤나 지웠나 | 무슨 일이 있었는지 모릅니다 |
| 바뀐 뒤 값 | 그 행이 지금 어떤 값인가 | 적용할 값이 없습니다 |
| 바뀌기 전 값 | 그 행이 직전에 어떤 값이었나 | 무엇이 달라졌는지 못 가립니다 |
| 위치 | 변경 기록에서 어디쯤인가 | 앞뒤 순서를 못 맞춥니다 |
다섯 칸을 채운 한 건은 이렇게 생겼습니다.
{
"table": "orders", // 어느 표
"op": "update", // 무슨 연산
"key": {"id": 8871}, // 어느 행
"before": {"status": "pending"}, // 전
"after": {"status": "paid"}, // 후
"pos": "0000391:42" // 기록 위치
}
주문 8871번의 상태가 결제 대기에서 결제됨으로 바뀌었습니다. 마지막 칸은 이 변경이 원본의 변경 기록에서 어디쯤인지를 가리킵니다. 캡처기가 멈췄다 다시 떴을 때 어디부터 이어 읽을지를 이 값으로 정합니다.
바뀌기 전 값은 안 보내는 경우도 있습니다. 무엇이 달라졌는지 가릴 필요가 없는 곳에서는 이 칸을 빼기도 합니다. 보내려면 데이터베이스가 그 값까지 변경 기록에 남기도록 설정해 두어야 합니다. 그만큼 기록이 커집니다.
전량 비교와 이중 쓰기
변경 데이터 캡처가 없을 때 같은 일을 하는 방법이 둘 있습니다. 둘 다 쓰이지만 각각 걸리는 데가 있습니다.
첫째는 원본을 통째로 다시 읽어 받는 쪽과 견주는 것입니다. 행이 백만 개면 백만 개를 다 읽어야 합니다. 읽는 동안 또 바뀐 것은 다음 차례로 밀립니다.
자주 돌릴수록 원본이 지는 부담이 늘어서 간격을 벌리게 됩니다. 간격을 벌리면 받는 쪽이 그만큼 더 뒤처집니다.
둘째는 애플리케이션이 쓸 때마다 두 곳에 각각 쓰는 것입니다. 데이터베이스에 한 번 쓰고 검색 색인에 한 번 더 씁니다. 이것을 이중 쓰기라고 부릅니다. 한쪽이 성공하고 다른 쪽이 실패하면 둘이 어긋난 채로 남는데, 어긋났다는 것을 알려 주는 쪽이 아무 데도 없습니다.
변경 데이터 캡처는 쓰는 곳을 하나로 되돌립니다. 애플리케이션은 데이터베이스에만 씁니다. 나머지 저장소는 그 결과로 남은 변경 기록을 읽어 따라갑니다.
변경을 잡아내는 세 갈래
어디에서 변경을 잡느냐에 따라 잡히는 것과 놓치는 것이 갈립니다. 변경이 들어오는 길이 하나가 아니기 때문입니다.
애플리케이션을 거쳐 들어온 쓰기가 있습니다. 사람이 콘솔에서 직접 고친 쓰기도 있습니다. 밤에 도는 배치가 한꺼번에 바꾸기도 합니다. 이 셋을 다 잡는지 일부만 잡는지가 갈래마다 다릅니다.
질의로 훑는 방법이 가장 만들기 쉽습니다. 표에 마지막으로 고친 시각을 적는 열을 둡니다. 주기마다 그 시각이 지난 행만 골라 읽습니다. 대신 지워진 행은 질의 결과에 안 나오므로 안 잡힙니다.
트리거는 어떤 표에 변경이 생길 때마다 데이터베이스가 알아서 실행하는 작은 절차입니다. 이 절차가 변경 내용을 별도의 변경 표에 한 줄로 적습니다. 캡처기는 원본 표가 아니라 그 변경 표를 읽습니다.
트리거는 변경이 어느 길로 들어왔는지를 가리지 않습니다. 애플리케이션을 거쳐 들어온 쓰기든 콘솔에서 직접 고친 것이든 똑같이 잡힙니다. 대신 트리거는 원본을 고치는 트랜잭션 안에서 같이 돕니다. 원본 쓰기가 끝나기 전에 트리거까지 실행돼야 해서 그만큼 원본 쓰기가 늦어집니다.
변경 기록을 읽는 방법은 데이터베이스가 이미 적어 둔 파일을 가져다 씁니다. 원본에 새로 얹는 일이 거의 없습니다. 순서도 삭제도 원본이 적어 둔 대로 나옵니다. 대신 기록 파일의 형식이 제품마다 달라서 읽는 쪽을 제품별로 따로 만들어야 합니다.
셋이 원본의 어디에 붙는지를 그리면 이렇습니다.
flowchart TD
subgraph SRC["원본 데이터베이스"]
W["쓰기 트랜잭션"] --> TBL["표"]
W --> LOG["변경 기록 파일"]
CT["변경 표"]
end
Q["질의로 훑기"] --> TBL
TR["트리거"] --> W
TR --> CT
RD["변경 기록 읽기"] --> LOG
질의로 훑기는 표를 밖에서 들여다봅니다. 트리거는 쓰기 트랜잭션 안에 끼어듭니다. 변경 기록 읽기는 이미 적힌 파일을 옆에서 읽습니다.
셋을 나란히 놓으면 이렇게 갈립니다.
| 갈래 | 잡히는 것 | 놓치거나 치르는 것 |
|---|---|---|
| 질의로 훑기 | 시각 열이 갱신된 행 | 삭제 · 주기 사이의 중간 상태 |
| 트리거 | 어느 길로 들어왔든 모든 변경 | 원본 쓰기에 걸리는 시간 |
| 변경 기록 읽기 | 순서와 삭제까지 원본이 적은 대로 | 제품마다 다른 기록 형식과 권한 설정 |
잡아내는 폭이 가장 넓은 것은 변경 기록 읽기입니다. 삭제와 순서까지 원본이 적은 대로 나오는 것은 셋 중 이것뿐입니다. 그래서 새로 놓는 흐름은 대개 변경 기록을 읽습니다.
변경 기록을 읽어 내보내는 순서
변경 기록을 읽는 갈래는 이렇게 돕니다. 애플리케이션은 평소대로 데이터베이스에만 씁니다.
flowchart TD
A["애플리케이션이 쓴다"] --> B["원본 데이터베이스"]
B --> C["변경 기록 파일"]
C --> D["캡처기가 읽는다"]
D --> E["변경 한 건씩 내보낸다"]
E --> F["검색 색인"]
E --> G["분석용 저장소"]
E --> H["캐시"]
그림에서 애플리케이션과 검색 색인 사이에는 선이 없습니다. 받는 쪽이 늘고 줄어도 애플리케이션 코드는 손대지 않습니다. 검색 색인이든 분석용 저장소든 캐시든 새로 붙이려면 같은 흐름에 하나 더 연결하면 됩니다.
캡처기는 어디까지 읽었는지를 따로 적어 둡니다. 그 표시가 없으면 다시 떴을 때 기록의 처음부터 읽거나 끝에서부터 읽게 됩니다. 앞쪽은 이미 보낸 변경을 또 보냅니다. 뒤쪽은 멈춰 있던 동안의 변경을 통째로 잃습니다.
처음 한 번의 스냅샷
변경 기록은 지나간 모든 변경을 갖고 있지 않습니다. 파일이 끝없이 커질 수 없어서 오래된 부분부터 지웁니다. 오늘 시작한 흐름이 재작년에 만들어진 행을 기록에서 찾을 방법은 없습니다.
그래서 시작할 때는 두 걸음을 밟습니다. 먼저 지금 표에 들어 있는 행을 통째로 한 번 읽어 받는 쪽에 옮깁니다. 이것이 스냅샷입니다. 그다음부터는 그 시점 이후의 변경 기록을 이어 읽습니다.
두 걸음 사이에 틈이 생기면 안 됩니다. 스냅샷을 뜨는 동안에도 원본은 계속 바뀌기 때문입니다. 스냅샷을 시작하는 위치를 먼저 적어 두고 스냅샷이 끝나면 그 위치부터 이어 읽는 식으로 틈을 메웁니다. 이러면 스냅샷에 이미 들어간 변경이 한 번 더 흘러옵니다.
sequenceDiagram
participant 캡처기
participant 원본 as 원본 데이터베이스
participant 받는쪽 as 받는 쪽
캡처기->>원본: 지금 위치를 적어 둔다
캡처기->>원본: 표를 통째로 읽는다(스냅샷)
캡처기->>받는쪽: 스냅샷을 옮긴다
Note over 원본: 스냅샷 뜨는 동안에도 변경은 계속 쌓인다
캡처기->>원본: 적어 둔 위치부터 변경 기록을 이어 읽는다
Note over 캡처기,받는쪽: 스냅샷에 이미 들어간 변경이 한 번 더 흘러온다
순서와 되풀이 전달
같은 행에 대한 변경은 닿는 순서가 곧 결과입니다. 같은 행의 상태가 결제 대기에서 결제됨으로, 다시 환불됨으로 바뀌었다면 이 셋이 닿는 순서가 뒤집히는 순간 받는 쪽의 값이 틀립니다. 그래서 한 행에 대한 변경만큼은 순서를 지켜 보냅니다.
같은 변경이 두 번 오는 것은 막기 어렵습니다. 캡처기가 변경을 내보낸 직후에 죽으면 어디까지 읽었는지 표시를 갱신하기 전이라, 다시 떠서 같은 건을 한 번 더 보냅니다. 표시를 먼저 갱신하면 반대로 그 건이 통째로 사라집니다.
두 실패 중 하나를 고르는 일입니다. 대개 다시 보내는 쪽을 고릅니다.
flowchart TD
A["어디서 죽었나"] --> B["내보낸 뒤 · 표시 갱신 전"]
A --> C["표시 갱신 뒤 · 내보내기 전"]
B --> D["같은 건을 한 번 더 보낸다 · 되풀이 전달"]
C --> E["그 건을 건너뛴다 · 유실"]
그래서 받는 쪽은 같은 변경을 두 번 받아도 결과가 같아지게 만듭니다. 값을 더하는 대신 행 전체를 덮어쓰는 식입니다. 이 성질을 멱등성이라고 부릅니다.
치르는 대가
첫째는 지연입니다. 원본이 바뀐 순간과 받는 쪽에 닿는 순간 사이가 벌어집니다. 그 사이에 둘을 나란히 읽으면 다른 값이 나옵니다.
주문을 저장하자마자 검색 결과에서 찾으면 안 나올 수 있습니다. 이렇게 시간이 지나면 같아지는 상태를 최종 일관성이라고 부릅니다.
둘째는 스키마 변경입니다. 원본 표에 열이 하나 늘면 그 뒤로 흘러오는 변경의 모양이 달라집니다. 받는 쪽이 옛 모양을 기대하고 있으면 그때부터 흐름이 멈춥니다. 어떤 모양으로 보낼지를 미리 정해 두고 양쪽이 같이 올라가도록 맞춰야 합니다.
셋째는 따라잡기입니다. 변경 기록은 오래된 부분부터 지워집니다. 캡처기가 며칠 멈춰 있는 동안 읽던 지점이 남아 있는 구간 밖으로 밀려나면 이어 읽을 곳이 없어집니다.
그때는 스냅샷부터 다시 해야 합니다. 그동안 받는 쪽은 낡은 값을 보여 줍니다.
마지막은 값이 흘러 나간다는 것입니다. 변경 기록에는 표에 들어 있던 값이 그대로 담깁니다. 비밀번호 해시나 개인 정보가 실린 열도 가리지 않고 따라 나갑니다. 내보내기 전에 어느 열을 지우거나 가릴지 정해 두어야 합니다.
관련 항목
변경 데이터 캡처가 읽어 내는 데이터베이스 기록
트랜잭션 로그 · WAL · binlog · redo 로그 · 복제 슬롯 · LSN
변경 데이터 캡처가 변경을 잡아내는 다른 방식
트리거 · 폴링 · 타임스탬프 · 버전 열 · 소프트 삭제 · 툼스톤
변경 데이터 캡처와 같은 문제를 다르게 푸는 설계
이중 쓰기 · 아웃박스 패턴 · 이벤트 소싱 · CQRS · 배치 ETL · 전량 적재
변경 데이터 캡처를 구현한 도구와 서비스
Debezium · Kafka Connect · Apache Kafka · AWS DMS · Oracle GoldenGate · Maxwell
변경 데이터 캡처가 변경을 실어 보내는 전달 경로
이벤트 스트림 · 메시지 큐 · 메시지 브로커 · 토픽 · 파티션 · 스트림 처리
변경 데이터 캡처가 변경을 흘려보내는 대상 저장소
검색 색인 · Elasticsearch · 데이터 웨어하우스 · 데이터 레이크 · 캐시 · 읽기 모델 · 구체화 뷰
변경 데이터 캡처가 지켜야 하는 전달 성질
멱등성 · 최소 한 번 전달 · 정확히 한 번 전달 · 순서 보장 · 오프셋 · 체크포인트
변경 데이터 캡처를 쓸 때 벌어지는 어긋남
최종 일관성 · 복제 지연 · 스키마 변경 · 스키마 레지스트리 · 백프레셔 · 재처리
변경 데이터 캡처가 놓이는 데이터 파이프라인 단계
원천 · 대상 · 수집 · 변환 · 적재 · 파이프라인 · 데이터 엔지니어링
변경 데이터 캡처가 실어 나르는 흐름을 쓰는 용도
다른 이름: Change Data Capture · CDC · 변경 데이터 수집 · 체인지 데이터 캡처