Designing Data-Intensive Applications(데이터 중심 애플리케이션 설계)
데이터 중심 애플리케이션 설계 장별 노트
- Designing Data-Intensive Applications(데이터 중심 애플리케이션 설계)

Designing Data-Intensive Applications(데이터 중심 애플리케이션 설계)
마틴 클레프만의 Designing Data-Intensive Applications를 장별 노트 형식으로 정리한다. 원서의 흐름을 따라 개념을 먼저 잡고, 각 장에서 설계자가 판단해야 하는 선택지를 함께 기록한다.
- San Diego Machine Learning Book Club 정리
- Alexandria playground
- DDIA 참고 문헌 저장소
- DDIA 운영 플레이북
- DDIA 플레이북 리허설 가이드
Chapter 1. 신뢰성, 확장성, 유지보수성 있는 애플리케이션
책의 구성
- 책은 세 부분으로 구성된다.
- 1부는 데이터 시스템의 기초를 다룬다.
- 2부는 여러 장비로 확장할 때 생기는 분산 데이터 문제를 다룬다.
- 3부는 배치 처리와 스트림 처리를 포함한 파생 데이터 통합을 다룬다.
데이터 시스템의 목표
- 오늘날 많은 애플리케이션은 계산 중심보다 데이터 중심이다.
- 데이터베이스에 데이터를 저장한다.
- 캐시에 자주 쓰는 결과를 기억한다.
- 검색 인덱스로 사용자가 데이터를 찾게 한다.
- 스트림 처리로 비동기 메시지를 다룬다.
- 배치 처리로 누적 데이터를 재계산한다.
- 데이터 시스템 설계는 단일 도구 선택이 아니라 여러 도구를 조합해 하나의 일관된 서비스를 만드는 일이다.
신뢰성
- 신뢰성은 문제가 생겨도 시스템이 계속 올바르게 동작하는 성질이다.
- 결함과 장애는 구분해야 한다.
- 결함은 시스템 일부가 사양에서 벗어난 상태다.
- 장애는 사용자에게 제공해야 하는 서비스 전체가 멈추거나 잘못 동작하는 상태다.
- 좋은 시스템은 결함을 완전히 없애기보다 결함이 장애로 확대되지 않게 만든다.
- 결함의 종류는 크게 세 가지다.
- 하드웨어 결함: 디스크 고장, 메모리 오류, 정전 등.
- 소프트웨어 오류: 버그, 자원 누수, 잘못된 의존성, 예외 처리 누락 등.
- 인적 오류: 설정 실수, 잘못된 배포, 운영 절차 누락 등.
확장성
- 확장성은 부하가 증가했을 때 시스템이 대응할 수 있는 능력이다.
- 먼저 부하를 설명하는 지표를 정해야 한다.
- 초당 요청 수, 읽기/쓰기 비율, 활성 사용자 수, 캐시 히트율, 팬아웃 수 등이 부하 매개변수가 된다.
- 성능은 평균보다 백분위로 보는 편이 낫다.
- p50은 일반 사용자 경험을 보여준다.
- p95, p99는 느린 사용자 경험과 꼬리 지연을 보여준다.
- 확장 방법은 상황에 따라 다르다.
- 수직 확장은 단순하지만 한계와 비용 증가가 있다.
- 수평 확장은 탄력적이지만 파티셔닝, 라우팅, 리밸런싱이 복잡해진다.
유지보수성
- 대부분의 소프트웨어 비용은 처음 만드는 데서 끝나지 않고 유지보수에서 발생한다.
- 유지보수성을 높이는 세 가지 축이 있다.
- 운용성: 운영자가 시스템 상태를 이해하고 복구할 수 있어야 한다.
- 단순성: 우발적 복잡도를 줄여야 한다.
- 발전성: 요구사항 변화에 맞게 쉽게 수정할 수 있어야 한다.
- 좋은 추상화는 구현 세부사항을 감추고, 잘못된 추상화는 복잡도를 더한다.
Chapter 2. 데이터 모델과 질의 언어
데이터 모델 계층
- 애플리케이션은 여러 데이터 모델을 층층이 쌓아 만든다.
- 애플리케이션 객체 모델.
- 데이터베이스의 논리 모델.
- 저장 엔진의 물리 모델.
- 디스크와 메모리의 바이트 표현.
- 데이터 모델은 단순한 저장 방식이 아니라 문제를 생각하는 방식을 결정한다.
관계형 모델과 문서 모델
- 관계형 모델은 데이터를 relation으로 표현한다.
- 구현 세부사항을 숨기고 질의 언어로 데이터 접근을 추상화한다.
- 조인과 정규화에 강하다.
- 문서 모델은 JSON/XML 같은 중첩 구조를 자연스럽게 표현한다.
- 한 번에 함께 읽는 데이터에 유리하다.
- 조인이 약하고 중복 데이터 관리가 필요하다.
- NoSQL의 등장은 여러 요구에서 비롯됐다.
- 대규모 데이터셋과 높은 쓰기 처리량.
- 오픈소스 분산 데이터베이스 선호.
- 관계형 스키마보다 유연한 데이터 모델.
- 특수 질의 패턴에 맞춘 성능.
정규화와 비정규화
- 정규화는 중복을 줄이고 일관성을 유지하기 좋다.
- 비정규화는 읽기 성능과 지역성을 높일 수 있다.
- 둘 중 하나가 항상 옳은 것은 아니다.
- 자주 함께 읽고 작게 변하는 데이터는 내장하기 좋다.
- 여러 곳에서 공유되고 자주 바뀌는 데이터는 참조가 낫다.
질의 언어
- 선언형 질의 언어는 원하는 결과를 말한다.
- SQL이 대표적이다.
- 실행 계획은 데이터베이스가 최적화한다.
- 명령형 코드는 결과를 얻는 절차를 직접 작성한다.
- 제어권은 크지만 최적화 여지가 줄어든다.
- 그래프 질의는 관계가 복잡할 때 유용하다.
- 소셜 그래프, 추천, 권한, 네트워크 같은 도메인에 적합하다.
스키마 진화
- 스키마는 고정된 진리가 아니라 시간이 지나며 바뀐다.
- 관계형 모델은 쓰기 시점에 스키마를 강제한다.
- 문서 모델은 읽기 시점에 스키마를 해석하는 경향이 있다.
- 실무에서는 접근 패턴을 먼저 정하고 그에 맞춰 모델과 인덱스를 설계해야 한다.
Chapter 3. 저장소와 검색
저장소 엔진의 기본 질문
- 데이터베이스는 쓰기와 읽기라는 두 작업을 효율적으로 처리해야 한다.
- 가장 단순한 데이터베이스는 로그에 키-값을 계속 추가하는 구조다.
- 쓰기는 빠르다.
- 읽기는 전체 로그를 뒤져야 하므로 느리다.
- 인덱스는 읽기를 빠르게 만들지만 쓰기 비용을 추가한다.
해시 인덱스
- 해시 인덱스는 메모리에 key -> file offset을 저장한다.
- 키-값 조회에는 빠르지만 범위 질의에는 약하다.
- 로그 세그먼트를 나누고 컴팩션으로 오래된 값을 제거할 수 있다.
- 메모리에 모든 키를 올릴 수 있는 경우에 적합하다.
SSTable과 LSM-Tree
- SSTable은 키로 정렬된 세그먼트 파일이다.
- 메모리의 멤테이블에 쓰고, 일정 크기가 되면 디스크에 정렬된 파일로 flush한다.
- 백그라운드 컴팩션으로 여러 SSTable을 병합한다.
- 장점은 순차 쓰기와 높은 쓰기 처리량이다.
- 단점은 읽기 시 여러 파일을 확인해야 할 수 있고 컴팩션 I/O가 발생한다는 점이다.
B-Tree
- B-Tree는 대부분의 관계형 데이터베이스에서 사용하는 대표적인 인덱스 구조다.
- 데이터를 고정 크기 페이지 단위로 관리한다.
- 읽기 지연이 예측 가능하고 범위 질의에 강하다.
- 쓰기 시 페이지 분할, WAL 기록, 랜덤 I/O 비용이 생긴다.
OLTP와 OLAP
- OLTP는 사용자 요청에 대한 낮은 지연과 높은 동시성을 중시한다.
- OLAP는 많은 행을 스캔하고 집계하는 분석 질의를 중시한다.
- 열 지향 저장소는 분석에 유리하다.
- 필요한 컬럼만 읽는다.
- 컬럼 단위 압축 효율이 높다.
- 벡터화 실행에 유리하다.
- 머터리얼라이즈드 뷰는 미리 계산한 결과를 저장해 읽기를 빠르게 하지만 갱신 비용이 든다.
Chapter 4. 부호화와 발전
데이터 부호화
- 애플리케이션은 메모리의 객체를 저장/전송 가능한 바이트로 바꿔야 한다.
- 이 과정을 직렬화 또는 부호화라고 한다.
- 부호화 형식은 호환성, 성능, 크기, 디버깅 편의성에 영향을 준다.
JSON, XML, CSV
- 사람이 읽기 쉽고 범용 도구가 많다.
- 숫자와 문자열 타입 처리에 애매함이 있다.
- 스키마가 약해 필드 누락이나 타입 오류를 늦게 발견할 수 있다.
- 바이너리 형식보다 크고 파싱 비용이 클 수 있다.
Thrift와 Protocol Buffers
- 스키마를 먼저 정의하고 바이너리로 인코딩한다.
- 필드 이름 대신 태그 번호를 사용해 크기를 줄인다.
- 필드를 추가할 때 optional/default를 사용하면 호환성을 유지할 수 있다.
- 한 번 사용한 태그 번호는 재사용하면 안 된다.
Avro
- writer schema와 reader schema를 분리해 해석한다.
- 데이터 파일에는 스키마를 함께 넣거나, 메시지에는 스키마 레지스트리 ID를 넣을 수 있다.
- 동적 언어와 데이터 파이프라인에 잘 맞는다.
호환성
- 후방 호환성은 새 코드가 예전 데이터를 읽을 수 있는 성질이다.
- 전방 호환성은 예전 코드가 새 데이터를 읽을 수 있는 성질이다.
- 롤링 배포에서는 신/구 코드가 동시에 존재하므로 양쪽 호환성이 중요하다.
- 안전한 스키마 변경의 기본 규칙은 다음과 같다.
- 필드는 optional로 추가한다.
- 삭제한 필드 번호는 재사용하지 않는다.
- 타입 변경은 가능한 피한다.
- 소비자가 모르는 필드는 무시할 수 있어야 한다.
데이터 흐름
- 데이터베이스에 저장되는 데이터는 오래 남기 때문에 호환성이 특히 중요하다.
- REST/RPC는 클라이언트와 서버가 독립적으로 배포될 수 있음을 전제해야 한다.
- 메시지 큐와 이벤트 로그는 생산자와 소비자의 버전 차이를 자연스럽게 만든다.
Chapter 5. 복제
복제의 목적
- 복제는 같은 데이터의 사본을 여러 노드에 유지하는 것이다.
- 목적은 여러 가지다.
- 장애가 나도 서비스를 계속하기 위해.
- 사용자와 가까운 곳에서 읽기 위해.
- 읽기 처리량을 높이기 위해.
- 복제는 가용성을 높이지만 일관성 문제를 만든다.
단일 리더 복제
- 쓰기는 리더가 받고 팔로워에게 변경 내용을 전파한다.
- 읽기는 리더 또는 팔로워에서 처리할 수 있다.
- 구현이 비교적 단순하고 널리 쓰인다.
- 문제는 리더 장애와 복제 지연이다.
- 장애 조치 중 데이터 손실이 생길 수 있다.
- 스플릿 브레인을 막아야 한다.
- 팔로워 읽기는 오래된 데이터를 반환할 수 있다.
동기와 비동기 복제
- 동기 복제는 팔로워 확인 후 쓰기를 성공 처리한다.
- 데이터 손실 위험이 낮다.
- 지연이 늘고 팔로워 장애가 쓰기를 막을 수 있다.
- 비동기 복제는 리더가 먼저 응답하고 나중에 전파한다.
- 빠르고 가용성이 높다.
- 장애 조치 시 최신 쓰기가 사라질 수 있다.
- 실무에서는 반동기 방식처럼 일부 팔로워만 동기로 두는 절충도 사용한다.
복제 지연 문제
- Read-your-writes: 사용자가 방금 쓴 값을 바로 읽어야 한다.
- Monotonic reads: 같은 사용자가 시간이 거꾸로 가는 읽기를 경험하면 안 된다.
- Consistent prefix reads: 원인보다 결과를 먼저 보는 일이 없어야 한다.
- 완화 방법은 리더 라우팅, 세션 스티키, 버전/타임스탬프 기반 읽기 선택 등이 있다.
다중 리더 복제
- 여러 리더가 쓰기를 받을 수 있다.
- 지리적으로 떨어진 데이터센터나 오프라인 클라이언트에 유리하다.
- 동시에 같은 데이터를 수정하면 충돌 해결이 필요하다.
- LWW는 단순하지만 데이터 손실이 가능하다.
- 애플리케이션 수준 병합이나 CRDT는 안전하지만 복잡하다.
리더리스 복제
- 특정 리더 없이 여러 복제본에 직접 읽고 쓴다.
- N개의 복제본 중 W개에 쓰고 R개에서 읽는 quorum을 사용한다.
- W + R > N이면 최신 쓰기와 읽기 집합이 겹친다.
- sloppy quorum과 hinted handoff는 가용성을 높이지만 일관성을 약화시킬 수 있다.
Chapter 6. 파티셔닝
파티셔닝의 목적
- 파티셔닝은 큰 데이터를 여러 노드에 나누어 저장하는 것이다.
- 목표는 저장 용량과 처리량을 확장하는 것이다.
- 잘못된 파티션 키는 핫스팟을 만든다.
키 범위 파티셔닝
- 키의 범위를 기준으로 데이터를 나눈다.
- 범위 질의와 정렬에는 유리하다.
- 시간순 키처럼 특정 범위에 쓰기가 몰리면 핫스팟이 생긴다.
- 파티션 경계는 데이터 분포에 따라 조정되어야 한다.
해시 파티셔닝
- 키의 해시값으로 파티션을 정한다.
- 데이터를 균등하게 분산하기 쉽다.
- 범위 질의는 여러 파티션을 조회해야 하므로 어려워진다.
- 복합 키를 사용해 분산과 정렬을 함께 달성할 수 있다.
보조 인덱스와 파티셔닝
- 로컬 인덱스는 각 파티션 안에서만 인덱스를 유지한다.
- 쓰기는 단순하다.
- 읽기는 여러 파티션을 조회해야 할 수 있다.
- 글로벌 인덱스는 전체 데이터를 기준으로 인덱스를 나눈다.
- 읽기는 효율적이다.
- 쓰기는 여러 파티션에 영향을 줄 수 있어 복잡하다.
리밸런싱
- 노드를 추가하거나 제거하면 데이터 배치를 바꿔야 한다.
- 리밸런싱은 가능하면 자동으로, 점진적으로, 서비스 중단 없이 이뤄져야 한다.
- 단순한 mod N 방식은 노드 수가 바뀔 때 대부분의 키가 이동하므로 좋지 않다.
- 고정 파티션 수, 동적 분할, 노드 비례 파티션 같은 전략을 사용할 수 있다.
요청 라우팅
- 클라이언트가 직접 파티션 위치를 알 수 있다.
- 라우팅 티어가 요청을 적절한 노드로 보낼 수 있다.
- 아무 노드에 보내고 내부에서 전달할 수도 있다.
- ZooKeeper/etcd 같은 코디네이션 시스템으로 파티션 메타데이터를 관리할 수 있다.
Chapter 7. 트랜잭션
트랜잭션의 역할
- 트랜잭션은 여러 읽기/쓰기를 하나의 논리 작업으로 묶는다.
- 오류, 충돌, 장애가 있어도 애플리케이션이 단순한 실패 모델을 사용할 수 있게 한다.
- ACID에서 일관성은 데이터베이스가 자동으로 보장하는 것만이 아니라 애플리케이션 불변식과 함께 정의된다.
격리 수준
- Read Committed는 dirty read와 dirty write를 막는다.
- Snapshot Isolation은 트랜잭션 시작 시점의 일관된 스냅샷을 읽는다.
- Repeatable Read라는 이름은 데이터베이스마다 의미가 다를 수 있다.
- Serializable은 실행 결과가 어떤 순차 실행과 같도록 보장한다.
동시성 이상
- Dirty read: 커밋되지 않은 데이터를 읽는다.
- Dirty write: 커밋되지 않은 쓰기를 덮어쓴다.
- Read skew: 서로 다른 시점의 데이터를 함께 읽어 일관되지 않은 상태를 본다.
- Lost update: 동시에 수정한 결과 중 하나가 사라진다.
- Write skew: 각 트랜잭션은 조건을 만족한다고 보고 쓰지만 함께 보면 불변식이 깨진다.
- Phantom: 조건에 맞는 행 집합이 트랜잭션 중간에 바뀐다.
직렬성 구현
- 실제 직렬 실행은 단순하지만 처리량이 낮다.
- 2PL은 락으로 충돌을 막지만 대기와 교착상태가 생긴다.
- Serializable Snapshot Isolation은 낙관적으로 실행하고 위험한 충돌을 감지해 중단한다.
- 불변식은 가능하면 UNIQUE, CHECK, FOREIGN KEY 같은 제약으로 표현하는 편이 안전하다.
분산 트랜잭션
- 2PC는 여러 노드의 커밋을 조정하지만 코디네이터 장애 시 블로킹될 수 있다.
- XA 트랜잭션은 이종 시스템을 묶을 수 있지만 운영 복잡도가 크다.
- 마이크로서비스에서는 SAGA, Outbox, CDC, 멱등 소비자를 조합해 최종 일관성을 설계하는 경우가 많다.
Chapter 8. 분산 시스템의 골칫거리
결함과 부분 실패
- 단일 머신에서는 성공 또는 실패가 비교적 명확하다.
- 분산 시스템에서는 일부 노드만 실패하거나, 응답이 늦거나, 메시지가 사라질 수 있다.
- 요청이 실패했는지, 처리됐지만 응답만 사라졌는지 알 수 없는 경우가 많다.
신뢰할 수 없는 네트워크
- 네트워크는 지연, 손실, 중복, 재정렬을 모두 일으킬 수 있다.
- 타임아웃은 실패 감지의 기본 도구지만 정확하지 않다.
- 타임아웃이 짧으면 오탐이 늘고, 길면 복구가 늦어진다.
- 재시도는 성공률을 높일 수 있지만 부하 증폭을 만들 수 있다.
- 재시도 가능한 작업은 멱등하게 설계해야 한다.
신뢰할 수 없는 시계
- 물리 시계는 NTP 보정, 스큐, 점프의 영향을 받는다.
- 경과 시간 측정에는 monotonic clock을 사용해야 한다.
- 이벤트 순서 판단에는 물리 시간만 의존하면 위험하다.
- Lamport clock과 버전 벡터는 인과 관계를 표현하는 데 도움을 준다.
프로세스 중단
- 프로세스는 GC, 스케줄링, 페이지 폴트, 가상화 환경 문제로 멈출 수 있다.
- 멈춘 프로세스가 자신이 아직 리더라고 생각하고 쓰기를 수행하면 데이터가 손상될 수 있다.
- 리스만으로는 충분하지 않을 수 있으며 펜싱 토큰이 필요하다.
지식과 진실
- 분산 시스템에서 노드는 다른 노드의 상태를 직접 알 수 없다.
- 알 수 있는 것은 메시지와 타임아웃으로 추정한 정보뿐이다.
- quorum은 여러 노드의 응답을 모아 더 안전한 결정을 내리는 방법이다.
- 시스템 모델을 명확히 해야 어떤 보장을 제공할 수 있는지 판단할 수 있다.
Chapter 9. 일관성과 합의
일관성 모델
- 일관성 모델은 애플리케이션이 데이터 시스템에서 기대할 수 있는 약속이다.
- 강한 모델은 추론을 쉽게 하지만 지연과 가용성 비용이 크다.
- 약한 모델은 빠르고 가용성이 높지만 애플리케이션이 더 많은 경우를 처리해야 한다.
선형성
- 선형성은 시스템이 데이터의 단일 복사본처럼 보이는 성질이다.
- 쓰기가 완료되면 이후 읽기는 그 값을 볼 수 있어야 한다.
- 분산 락, 리더 선출, 유일성 제약에는 선형성이 중요하다.
- 네트워크 파티션 상황에서는 선형성과 가용성을 동시에 완전히 만족하기 어렵다.
순서화 보장
- 순서는 인과성을 이해하는 핵심이다.
- Lamport timestamp는 happens-before 관계를 반영하지만 동시 사건을 완전히 구분하지 못한다.
- 전체 순서 브로드캐스트는 모든 노드가 같은 순서로 메시지를 처리하게 한다.
- 전체 순서 브로드캐스트와 합의는 서로 밀접하게 연결된다.
분산 트랜잭션과 합의
- 원자적 커밋은 여러 노드가 모두 커밋하거나 모두 중단하도록 만드는 문제다.
- 2PC는 코디네이터에 의존하므로 장애 시 블로킹될 수 있다.
- 합의 알고리즘은 노드들이 하나의 값에 동의하게 한다.
- Raft, Paxos, Zab은 리더 기반으로 로그 복제를 수행한다.
멤버십과 코디네이션
- ZooKeeper/etcd 같은 시스템은 합의 기반으로 작은 메타데이터를 안전하게 관리한다.
- 리더 선출, 서비스 디스커버리, 설정 배포, 분산 락에 사용된다.
- 락을 사용할 때는 펜싱 토큰이 함께 필요하다.
Chapter 10. 일괄 처리
배치 처리의 성격
- 배치 처리는 유한한 입력을 읽고 결과를 만든다.
- 입력은 보통 불변이고, 같은 입력과 같은 코드라면 같은 결과를 다시 만들 수 있다.
- 온라인 시스템과 달리 지연 시간보다 처리량과 재현성이 중요하다.
Unix 철학
- 작은 프로그램이 하나의 일을 잘 수행하고, 표준 입출력으로 연결된다.
- 입력을 직접 수정하지 않고 새 출력을 만든다.
- 이 방식은 실험, 디버깅, 재실행에 강하다.
- 데이터 파이프라인 설계에도 같은 원칙이 적용된다.
MapReduce
- Map은 입력 레코드를 key-value 쌍으로 바꾼다.
- Shuffle은 같은 key를 같은 reducer로 모은다.
- Reduce는 key별 값을 집계한다.
- 중간 결과를 디스크에 쓰기 때문에 내결함성은 좋지만 반복 작업에는 비효율적이다.
조인과 그룹화
- 정렬 병합 조인은 큰 데이터셋을 key 기준으로 정렬해 병합한다.
- Broadcast hash join은 작은 테이블을 모든 작업자에 복제한다.
- Partitioned join은 양쪽 데이터를 같은 key 기준으로 나눈다.
- 데이터 skew가 있으면 특정 reducer에 부하가 몰릴 수 있다.
현대 배치 엔진
- Spark, Flink, Tez는 MapReduce보다 유연한 DAG 실행 모델을 제공한다.
- 중간 결과를 메모리에 유지해 반복 작업을 빠르게 할 수 있다.
- 여전히 실패 복구, 데이터 지역성, shuffle 비용을 고려해야 한다.
배치 출력
- 배치 작업은 부분 결과를 노출하지 않는 것이 중요하다.
- 임시 경로에 쓰고 성공 시 원자적으로 교체하는 방식이 안전하다.
- 재처리와 백필을 고려해 출력 버전과 파티션을 관리해야 한다.
Chapter 11. 스트림 처리
스트림의 의미
- 스트림은 시간이 지나며 계속 도착하는 이벤트의 흐름이다.
- 배치가 유한 데이터셋을 다룬다면 스트림은 무한 데이터셋을 다룬다.
- 데이터베이스의 변경 로그도 스트림으로 볼 수 있다.
메시지 전달 보장
- At-most-once는 중복은 없지만 유실이 가능하다.
- At-least-once는 유실은 줄지만 중복이 가능하다.
- Exactly-once는 경계가 명확한 시스템 안에서는 가능할 수 있지만 end-to-end로는 어렵다.
- 실무에서는 멱등성, 트랜잭션, 체크포인트를 조합해 효과적 exactly-once를 만든다.
이벤트 시간과 처리 시간
- 이벤트 시간은 사건이 실제로 일어난 시간이다.
- 처리 시간은 시스템이 이벤트를 처리한 시간이다.
- 지연 이벤트 때문에 결과가 늦게 보정될 수 있다.
- 워터마크는 어느 시점 이전의 이벤트가 대부분 도착했다고 추정하는 장치다.
윈도우
- Tumbling window는 겹치지 않는 고정 구간이다.
- Hopping/sliding window는 겹치는 구간이다.
- Session window는 사용자 활동의 끊김을 기준으로 구간을 만든다.
- 윈도우 결과는 늦게 도착한 이벤트를 어떻게 처리할지 정책이 필요하다.
스트림 조인
- Stream-stream join은 양쪽 이벤트를 일정 시간 동안 상태로 보관해야 한다.
- Stream-table join은 이벤트를 현재 테이블 상태로 보강한다.
- Table-table join은 양쪽 변경에 따라 결과가 계속 갱신된다.
- 상태 크기, 체크포인트, 복구 시간을 함께 고려해야 한다.
CDC와 파생 데이터
- CDC는 데이터베이스 변경을 이벤트 스트림으로 내보낸다.
- 검색 인덱스, 캐시, 분석 저장소 같은 파생 데이터를 갱신하는 데 유용하다.
- CDC 지연과 재처리 전략을 운영 관점에서 설계해야 한다.
Chapter 12. 데이터 시스템의 미래
데이터 시스템의 조합
- 단일 데이터베이스가 모든 요구를 만족하기는 어렵다.
- 현대 시스템은 OLTP DB, 검색 엔진, 캐시, 메시지 로그, 분석 저장소를 조합한다.
- 조합이 늘어날수록 데이터 통합과 일관성 관리가 중요해진다.
파생 데이터
- 파생 데이터는 원천 데이터에서 계산되어 만들어진 데이터다.
- 검색 인덱스, 캐시, 머터리얼라이즈드 뷰, 분석 테이블이 여기에 해당한다.
- 파생 데이터는 잃어도 원천에서 다시 만들 수 있어야 한다.
- 원천 데이터와 파생 데이터 사이의 지연과 불일치를 관측해야 한다.
데이터베이스의 분해
- 전통적 데이터베이스는 저장, 색인, 질의, 트랜잭션을 하나의 시스템에 묶었다.
- 로그 기반 아키텍처는 변경 이벤트를 중심에 두고 여러 읽기 모델을 만든다.
- 이벤트 소싱은 상태 변화 자체를 이벤트로 저장한다.
- 장점은 감사와 재처리이고, 단점은 쿼리 복잡도와 운영 난이도다.
Lambda와 Kappa
- Lambda 아키텍처는 배치 경로와 스트림 경로를 함께 둔다.
- 배치는 정확하고 재처리에 강하다.
- 스트림은 낮은 지연을 제공한다.
- 두 경로를 모두 유지해야 하는 비용이 있다.
- Kappa 아키텍처는 스트림 로그 하나를 중심으로 재처리도 리플레이로 해결한다.
- 단순하지만 스트림 시스템의 보존 기간과 재처리 비용을 고려해야 한다.
End-to-End 정확성
- 시스템 내부 구성요소가 각각 정확해도 전체 경로가 정확하다는 보장은 없다.
- 클라이언트 재시도, 서버 처리, DB 커밋, 이벤트 발행, 소비자 처리까지 end-to-end로 봐야 한다.
- 멱등키, 원자적 쓰기, outbox, 소비자 중복 제거가 함께 필요하다.
- 불변식은 동기 제약과 비동기 감사로 모두 관리할 수 있다.
신뢰, 개인정보, 윤리
- 데이터 시스템은 사용자에 대한 권력을 가진다.
- 수집 목적과 보존 기간을 제한해야 한다.
- 삭제 요청과 감사 가능성을 설계에 포함해야 한다.
- 기술적 정확성뿐 아니라 데이터 사용의 정당성도 시스템 설계의 일부다.