❒ 개요
- 이번 장에서는 미래에는 어떻게 돼야 하는지에 대해서 설명함.
- 앞에서 배운 아이디어들을 함께 모아 그것을 기반으로 미래를 고찰하자.
❒ 1. 데이터 통합
- 가장 적절한 소프트웨어 도구를 선택하는 것은 상황에 따라 다름.
- 선택의 폭이 넓은 경우
- 소프트웨어 제품과 그 제품고가 잘 어울리는 환경 사이의 대응 관계를 파악하는 것
- 복잡한 환경에서는 데이터를 여러 가지 다른 방법으로 사용하기 때문에, 소프트웨어가 모든 상황에 대처할 가능성이 낮음.
🌀 1-1. 파생 데이터에 특화된 도구의 결합
개요
- OLTP용 데이터베이스(트랜잭션 처리 DB)만으로는 임의 키워드 검색(full-text search) 같은 걸 잘 처리하기 어려움.
- PostgreSQL 같은 DB도 풀텍스트 기능을 제공하지만
- 복잡하고 정교한 검색을 하려면 Elasticsearch, Solr 같은 전문 검색 도구가 더 적합
- 반대로, 검색 인덱스는 system of record로 쓰기엔 내구성/일관성 측면에서 좋지 않음.
- 따라서, 중요한 데이터는 여전히 RDB에 저장해야 함.
- 그래서 실무에서는 아래의 도구를 조합해서 요구사항을 만족 시킴
- “쓰기/정합성”은 DB
- “검색/질의”는 검색엔진
- 같은 비즈니스 데이터가 여러 시스템에 복제·변환돼 존재하기 때문에
- 각 시스템을 어떻게 통합하고 동기화할지가 점점 더 어려움.
데이터플로에 대한 추론 (Reasoning about dataflows)
- 모든 쓰기의 순서를 결정하는 단일 시스템으로 모든 사용자 입력을 밀어 넣을 수 있다면
- 쓰기를 같은 순서로 처리해 데이터를 다른 표현형으로 파생하기가 훨씬 쉬워짐
파생 데이터 vs 분산 트랜잭션
- 추상적인 수준에서 보면 파생 데이터와 분산 트랜잭션은 다른 방식으로 유사한 목표를 달성함.
- 분산 트랜잭션
- 상호 배타적인 잠금을 사용해 순서를 결정
- 원자적 커밋을 사용해 변경 효과가 정확히 한 번 나타나도록 보장
- 파생 데이터
- 로그를 사용해서 순서를 결정
- 결정적 재시도와 멱등성을 기반으로 함.
- 분산 트랜잭션
- 두 시스템의 가장 큰 차이점
- 트랜잭션 시스템
- 선형성을 지원함.
- 파생 데이터 시스템
- 비동기로 갱신되기 때문에 기본적으로 동시간 갱신 보장을 하지 않음.
- 트랜잭션 시스템
전체 순서화의 제약 (The limits of total ordering)
- 모든 이벤트를 하나의 글로벌 순서로 정하는 것은 작고 단순한 시스템에서는 가능
- 하지만 파티셔닝, 다중 데이터센터, 마이크로서비스, 오프라인 클라이언트 등 현실적인 요구가 늘어나면
전역 total order를 유지하는 것이 근본적으로 어렵고, 현재 합의 알고리즘으로도 한계가 있다
인과성 획득을 위한 이벤트 순서화
➔ 모든 이벤트를 전역 순서로 정하지 못하더라도, 중요한 ‘인과관계(causality)’만큼은 어떻게 보존할 것인가?
- 이벤트 간 인과성이 없는 경우 전체 순서가 정해지지 않아도 큰 문제가 아님.
- 동시에 발생한 이벤트는 임의의 순서로 정할 수 있기 때문임.
- 예시 : 친구 끊고, 뒷담
- 친구 상태를 저장하는 곳과 메시지를 저장하는 곳이 다른 시스템의 경우
- 친구 끊기 이벤트와 메시지 보내기 이벤트 사이의 순서 의존성이 없음.
- 이 경우, 뒷담 깐거를 들길 수 있음.
- 친구 상태를 저장하는 곳과 메시지를 저장하는 곳이 다른 시스템의 경우
- 이 문제를 해결할 수 있는 3가지 방법 제시
- 논리적 타임스탬프(Logical timestamps)
- “사용자가 본 상태”를 이벤트로 기록하기
- 충돌 해소(Conflict resolution) 알고리즘
🌀1-2. 일괄 처리와 스트림 처리
필자는...
- 데이터 통합의 목표는 데이터를 올바른 저장소에 올바른 형태로 두는 것이라고 생각함.
- 이렇게 하기 위해서는 아래의 과정을 거쳐야 함.
- 입력 ➔ 형 변환 ➔ 필터링 ➔ 집계 ➔ 모델 학습 ➔ 평가 ➔ 출력
- 일괄 처리자와 스트림 처리자는 이 목표를 달성하기 위한 도구임.
파생 상태 유지
- 입력과 출력을 잘 정의한 결정적 함수의 원리는
- 내결함성에 도움이 됨.
- 조직 내의 데이터플로 추론을 단순화 함.
- 이론상으로 파생 데이터 시스템은
- 관계형 DB가 색인할 테이블에 기록하는 트랜잭션 내에서 보조 색인을
동기식으로 갱신하는 것처럼 동기식으로 운영할 수 있음. - 하지만 비동기 방식을 사용하면 이벤트 로그 기반 시스템을 훨씬 견고하게 만들 수 있음.
- 관계형 DB가 색인할 테이블에 기록하는 트랜잭션 내에서 보조 색인을
- 분산 트랜잭션은
- 참여 장비 일부가 실패하면 어보트하기 때문에 나머지 시스템으로 실패가 확산되어 실패가 증폭되는 경향이 있음.
애플리케이션 발전을 위한 데이터 재처리
- 기존 데이터를 재처리하는 것은 시스템을 유지보수하기 위한 좋은 메커니즘으로
- 새로운 기능 추가와 요구사항에 대응할 수 있게 만듬.
- 재처리 없이 스키마를 변경하는 작업은
- 레코드에 새 선택적 필드를 추가하거나
- 새로운 타입의 레코드를 추가하는 것과 같은 간단한 것으로 제한됨.
- 이런 제약은 읽기 스키마와 쓰기 스키마 모두에 해당
- 파생뷰를 사용하면 점진적 발전이 가능함.
- 데이터셋을 재구축해야 할 경우 갑자기 뷰 이전을 수행할 필요가 없음.
- 신/구 버전의 독립적인 파생 뷰를 만들 수 있음.
- 즉, 뭔가 잘못됐을 때 쉽게 롤백할 수 있음.
람다 아키텍쳐
- 일괄 처리를 과거 데이터를 재처리하는데 사용하고, 최근 갱신 데이터를 처리하는데 스트림 처리르 사용
- 핵심 아이디어
- 람다 아키텍처를 쉽게 설명하면, 들어오는 모든 데이터를 삭제하거나 수정하지 않고 일단 저장해두고,
필요할 때마다 저장된 데이터를 다양하게 분석하거나 복구하는 구조라고 이해하면 됨. - 즉, 데이터를 바꾸거나 지우지 않고 계속 추가만 해서 "모든 기록을 남겨두는 방식"
- 실제로 잘못된 결과가 나오거나 새로운 분석 방법이 생겼을 때도 원본 데이터를 바탕으로 다시 계산하거나,
원하는 정보로 바꿀 수 있음.
- 람다 아키텍처를 쉽게 설명하면, 들어오는 모든 데이터를 삭제하거나 수정하지 않고 일단 저장해두고,
- 이 아키텍쳐에서 스트림 처리자는
- 이벤트를 소비해 근사 갱신을 뷰에 빠르게 반영함.
- 이후에 일괄 처리자가 같은 이벤트 집합을 소비해 정확한 버전의 파생 뷰에 반영함.
- 이 아키텍쳐의 설계 배경은
- 일괄 처리는 간단해서 버그가 생길 가능성이 적음.
- 반면에 스트림 처리자는 신뢰성이 떨어지고 내결함성을 확보하기 어렵다는 것.
- 근데 필자는 람다 아키텍쳐에 문제가 있다고 생각함
- 배치 코드 + 스트림 코드를 두 벌 유지해야 함.
- 전체 재처리는 비싸서, 결국 “증분 배치”가 필요해짐. (특정 시간의 데이터만 배치 처리하는 등..)
일괄 처리와 스트림 처리의 통합
- 최근에는 같은 시스템에서 일괄 처리 연산과 스트림 연산을 모두 구현함으로써
람다 아키텍처의 단점을 빼고 장점만 취할 수 있게 하는 작업이 진행되고 있음. - 배치와 스트림을 한 시스템으로 통합하려면 다음 세 가지 기능이 필요함.
- 최근 이벤트 스트림을 다루는 처리 엔진에서 과거 이벤트를 재생하는 능력
- 스트림 처리에서도 Exactly-once semantics 보장
- 이벤트 시간(event time) 기준 윈도우링 지원
- 위 세 가지 기능을 가지고 있다면
- 람다 아키텍처처의 단점은 없앨 수 있고,
- 하나의 스트리밍/데이터플로우 엔진이
- 실시간 처리, 과거 재처리, 파생 뷰 유지 를 모두 담당하는 통합 모델을 만들 수 있음.
❐ 2. 데이터베이스 언번들링 (데이터베이스의 분리)
- 저자는 운영체제(Unix) 와 데이터베이스(DB) 를 비교하면서
두 시스템이 같은 목적을 다른 철학으로 해결한 역사적 흐름을 설명하는 파트 - Unix는 단순하지만 직접 해야 할 게 많음
- DB는 강력하지만 내부가 감춰져 있음.
- 현대 시스템은 이 둘의 장점을 결합해 “유연하면서도 강력한 데이터 시스템”을 지향함.
- 이것이 바로 데이터베이스 언번들링의 출발점
🌀 2-1. 데이터 저장소 기술 구성하기
데이터베이스가 제공하는 다양한 기능에 대한 요약 설명
- 보조 색인은 필드 값을 기반으로 레코드를 효율적으로 검색할 수 있는 기능이다.
- 구체화 뷰는 질의 결과를 미리 연산한 캐시의 일종이다
- 복제 로그는 데이터의 복사본을 다른 노드에 최신 상태로 유지하는 기능이다.
- Full-text 검색 색인은 텍스트에서 키워드 검색을 가능하게 하는 기능이다.
색인 생성하기
- 필자는 인덱스 생성이 단순한 DB 내부 작업이 아니라,
- 데이터 재처리(reprocessing) 와 파생 데이터(derived data) 의 일종임을 강조
- CREATE INDEX 실행 시 일어나는 일
- 일관된 스냅샷(consisent snapshot) 확보
- 인덱스를 생성하려면, 특정 시점의 테이블 상태를 완전하게 읽어야 함.
- DB는 트랜잭션 격리 수준을 활용해 일관된 시점의 데이터 복사본(snapshot) 을 확보.
- 인덱싱할 필드 추출 및 정렬
- 예: CREATE INDEX ON users(email)
- 모든 users.email 값을 읽어서 정렬 후, B-Tree 혹은 다른 인덱스 구조로 저장.
- 인덱스 파일 쓰기
- 정렬된 데이터를 기반으로 새로운 인덱스 파일을 디스크에 기록.
- 스냅샷 이후에 발생한 쓰기(write) 처리
- 인덱스 생성 중에도 테이블은 계속 쓰기될 수 있음.
- DB는 “스냅샷 이후 발생한 변경분(backlog)”을 추적하여 새 인덱스에 반영.
- 지속적인 동기화
- 인덱스 생성이 끝나면, 이후 트랜잭션이 테이블에 쓰기할 때마다 인덱스도 함께 갱신..
- 일관된 스냅샷(consisent snapshot) 확보
- 즉, 새 인덱스를 만든다는 것은
- 기존 데이터를 전부 복제해서 새로운 형태로 저장하는 것과 같음
모든 것의 메타데이터베이스 (The meta-database of everything)
- 일괄 처리와 스트림 처리로 유지하는 파생 데이터 시스템은 마치 다양한 색인 유형과 비슷함.
- 기존에는 하나의 DB 엔진 안에 모든 인덱스 기능 포함
- B-tree, Hash, Spatial 등
- 각 기능을 독립된 전문 시스템으로 분리(Unbundling) 하는 방향으로 진화
- 텍스트 검색 : Elasticsearch, OpenSearch
- 이벤트 스트림 저장 : Kafka
- ...
- 필자는 서로다른 저장소와 처리 도구를 사용하지만 하나의 응집된 시스템으로
구성할 수 있는 2가지 방법이 있다고 생각함.- 연합 데이터 베이스 : 읽기를 통합
- 여러 종류의 저장 엔진과 처리 방식을 대상으로 하나의 통합 쿼리 인터페이스를 제공하는 접근.
- 즉, 다양한 데이터 소스를 하나의 SQL처럼 읽을 수 있게 하는 방식.
- 쓰기(write) 일관성 유지나 트랜잭션 처리에는 약함
- PostgreSQL Foreign Data Wrapper (FDW)\
- 외부의 다른 데이터 소스(MySQL, CSV, REST API 등)를 PostgreSQL 테이블처럼 읽을 수 있음.
- 언번들링 데이터베이스 : 쓰기를 통합
- 여러 저장소 간에 쓰기(write) 를 일관되게 동기화(synchronize) 하는 문제를 다룸.
- 즉, “데이터 변경을 여러 시스템에 안정적으로 반영” 하는 방식.
- Change Data Capture (CDC), Event Log, Outbox Pattern 등을 통한 데이터 동기화.
- 예: 트랜잭션 로그를 Kafka로 보내고, 이를 검색 인덱스나 캐시 시스템이 구독함.
- 연합 데이터 베이스 : 읽기를 통합
언번들링이 동작하게 만들기
- Federation vs Unbundling — “같은 동전의 양면”
- Federation : 여러 데이터 소스를 하나의 인터페이스로 읽기(read) 통합
- Unbundling : 여러 데이터 시스템 간 쓰기(write) 를 일관되게 동기화(synchronize)
→ 둘 다 이질적인 시스템을 결합(composition) 한다는 점에서 동일한 목표를 가진다.
- 쓰기 동기화(Write Synchronization)는 훨씬 어려움
- 읽기 통합은 단지 “데이터 모델 간 매핑(mapping)” 문제라서 상대적으로 단순함.
- 하지만 쓰기 통합은 여러 시스템에 걸쳐 데이터를 정합성 있게 동시에 반영해야 함.
- 즉, “한쪽에 쓴 내용이 다른 쪽에도 반영되어야 한다”는 문제는 훨씬 복잡하다.
- 전통적 접근: 분산 트랜잭션(Distributed Transactions)
- 과거에는 여러 저장소에 걸친 쓰기를 2PC 등의 분산 트랜잭션으로 처리하려고 했다.
- 하지만 이 방식은 다음과 같은 문제가 있음:
- 이기종 시스템 간에는 표준화된 트랜잭션 프로토콜이 없음.
- 네트워크 지연, 장애 시 전체 시스템이 멈출 위험(결합도가 너무 높음).
- 따라서 확장성과 견고성이 떨어지는 구조가 됨.
- 추천 접근: 이벤트 로그(Event Log) 기반의 비동기 통합
- 저자는 대신 다음 방식을 제안 ➔ “비동기 이벤트 로그 + 멱등(idempotent) 쓰기”
- 즉, 데이터를 쓸 때 즉시 여러 시스템에 쓰는 대신,
- 하나의 이벤트 로그에 변경을 기록하고
- 다른 시스템들은 이 로그를 구독(consume)하며 상태를 갱신
- 이 방식은 Derived Data System, Change Data Capture(CDC) 와 같은 형태로 구현된다.
- 왜 이벤트 로그 방식이 더 나은가
- 시스템 수준의 이점 - 느슨한 결합
- 각 구성 요소가 비동기적으로 통신하므로, 일부 시스템이 느리거나 장애가 나도 전체가 멈추지 않음.
- 이벤트 로그가 버퍼 역할을 하며 데이터 손실을 방지.
- 장애가 복구되면 소비자(consumer) 는 이벤트를 재처리(catch up) 가능.
- 반대로 분산 트랜잭션은 동기적으로 묶여 있어서 부분 장애가 전체 장애로 확산(escalation) 되기 쉽다.
- 조직 수준의 이점 — 독립 개발과 유지보수
- 각 팀이 자신의 시스템에만 집중할 수 있다.
- 팀 간 인터페이스(계약)가 명확해짐.
- 시스템 수준의 이점 - 느슨한 결합
- 이벤트 로그의 핵심 속성 (아래 속성이 결합되어 분리된 시스템 간 강력한 일관성을 유지하는 기능하게 함)
- 내구성 (Durability)
- 순서 보장 (Ordering)
- 멱등성 (Idempotence)
- 결론
- Unbundling(분리된 데이터 시스템의 통합) 을 가능하게 만드는 핵심은
분산 트랜잭션이 아니라 비동기 이벤트 로그(asynchronous log) 기반의 통합이다. - 시스템적으로는 느슨한 결합과 복원력(resilience)을 확보하고,
- 조직적으로는 독립된 개발·운영을 가능하게 한다.
- 결국 “로그(Log)”는 데이터 통합의 중심 축이자,
현대 분산 아키텍처에서 데이터 일관성을 유지하는 가장 현실적인 수단이다.
- Unbundling(분리된 데이터 시스템의 통합) 을 가능하게 만드는 핵심은
언번들링 vs 통합 시스템
- 저자는 “Unbundling이 미래의 방향”이라고 말하면서도,
통합형 데이터베이스의 역할이 여전히 필수적임을 강조
즉, “모든 걸 분리하라”가 아니라 “필요할 때만 분리하라” - Unbundling이 대체가 아니라 “보완”
- “Unbundling(분리형)” 접근이 데이터베이스를 완전히 대체하지는 않는다.
- 즉, 스트림/배치 시스템의 결과를 저장하고 서빙(Serving) 하는 역할은 여전히 DB가 맡는다.
- 필요없는 확장은 낭비
뭐가 빠졌지?
- 우리는 Kafka, DB, Search, Stream Processor 등 훌륭한 부품을 이미 갖고 있음
- 하지만 이들을 Unix 파이프처럼 단순하게 조합할 “언어(shell)” 가 아직 없다.
🌀 2-2. 데이터플로 주변 애플리케이션 설계
- 데이터 파생(derivation)은 모든 시스템의 핵심 동작이다.
- 하지만 단순 인덱스 생성 외의 복잡한 파생 로직은 애플리케이션 코드로 직접 구현해야 한다.
- 대부분의 데이터베이스는 이러한 “파생 로직(derivation function)”을 내장적으로 다루지 못한다.
➔ 이는 Unbundled Architecture 의 핵심 과제 중 하나다.
파생 함수로서의 애플리케이션 코드
- 모든 파생 데이터는 원본 데이터를 변환 함수(transformation function) 를 통해 얻어짐.
- 자동화된 파생 함수 vs 커스텀 파생 함수
- 자동화된 파생 함수
- 보조 인덱스(secondary index) 생성은 너무 자주 쓰이기 때문에
DB가 내부적으로 자동 처리 기능으로 내장 (CREATE INDEX). - 즉, derivation function이 DB 엔진에 내장되어 있음.
- 보조 인덱스(secondary index) 생성은 너무 자주 쓰이기 때문에
- 커스텀 파생 함수
- 전문 검색, 머신러닝, 캐시 구축 등은 도메인 특화 로직이 필요함.
- 따라서 표준화된 기능이 아니라 직접 코드로 구현해야 함.
- 이 때 애플리케이션 코드가 derivation function 역할을 수행.
- 자동화된 파생 함수
애플리케이션 코드와 상태의 분리
- “DB는 데이터 저장에, 애플리케이션은 코드 실행에” → 역할 분리가 합리적
- “상태(state)는 DB에, 로직(logic)은 코드에” → 이것이 오늘날의 기본적인 시스템 구조
- DB는 “공유 가능한 가변 변수(shared mutable variable)” 처럼 작동
- 하지만 문제는 DB 변경을 실시간으로 감지(subscribe) 하기 어려움
- 대부분의 DB는 수동적(polling) 방식: “값이 바뀌었는지”를 주기적으로 쿼리해야 함.
- 최근의 CDC·Change Stream 기술은 이 간극을 메워, DB와 애플리케이션 간의 진정한 분리가 가능함
데이터플로 : 상태 변경과 애플리케이션 코드 간 상호작용
➔ Unbundled Database = Dataflow 시스템으로 재조립된 DB
- 기존의 관계 — “코드가 상태를 조작한다”
- 전통적으로 우리는 DB를 단순한 상태 저장소(state holder) 로 봄
- 애플리케이션이 DB를 읽고 → 로직 처리 후 → DB에 다시 쓴다.
- 즉, 애플리케이션은 명령어 중심(command-driven) 으로 동작하고,
- DB는 수동적(passive) 역할에 머문다.
- 새로운 관점 — “상태 변화와 코드의 상호작용(interplay)”
- 이제는 DB의 상태 변화 자체를 이벤트(event) 로 보고,
- 애플리케이션 코드가 그 변화를 구독(subscribe) 하여 반응하는 구조로 변화.
- 예:
- 사용자가 주문 생성 → “OrderCreated” 이벤트 발생
- 코드가 이 이벤트를 받아 → 결제 요청, 재고 차감 등의 후속 상태 변화(trigger)
- 즉, 하나의 상태 변화가 또 다른 상태 변화를 유도하는 체계적인 데이터 흐름.
- 역사적·구조적 맥락
- 이 개념은 “데이터베이스와 스트림은 동등하다” 는 책의 핵심 논의에서 이어진다.
- DB의 트랜잭션 로그(log)는 곧 이벤트 스트림(event stream)이다.
- 따라서 로그를 구독하면 DB의 상태 변화를 실시간으로 감지할 수 있다.
- 비슷한 아이디어가 이미 오래전부터 존재했음:
- Actor 모델 (메시지 기반 동시성)
- Tuple space 모델 (프로세스가 공유 상태의 변화에 반응)
- Triggers & Secondary Indexes (DB 내부의 자동 반응 로직)
- 이 개념은 “데이터베이스와 스트림은 동등하다” 는 책의 핵심 논의에서 이어진다.
- Unbundling 관점에서 본 Dataflow
- DB 내부에서만 일어나던 “상태 변화에 대한 반응(trigger)”을 외부 시스템으로 확장하는 것.
- 예:
- DB → Kafka (Change Event)
- Kafka → Elasticsearch (검색 인덱스 업데이트)
- Kafka → ML Pipeline (모델 업데이트)
- Kafka → Cache (UI 캐시 리빌드)
- 이 모든 과정이 데이터플로우(dataflow) 로 연결된다.
- 즉, “상태 변화 → 코드 반응 → 새로운 상태 생성”의 연쇄.
- 데이터플로우와 일반 메시징 시스템의 차이점
- 파생 데이터를 유지하기 위해선 아래 두 가지 조건이 매우 중요함.
- 순서 보장
- 내구성 & 내결함성
- 파생 데이터를 유지하기 위해선 아래 두 가지 조건이 매우 중요함.
- Stream Processor = 현대의 파생 함수 엔진
- 각 스트림 연산자(operator)는 “데이터 변화”를 입력으로 받아 새로운 상태를 산출하는 “함수” 로 동작.
스트림 처리자와 서비스
- 요즘 개발 스타일 트렌드는
- 각 기능을 동기 네트워크 요청을 통해 통신하는 서비스의 집합으로 나누는 것
- 느슨한 연결을 통한 조직적 확장성
- 새로운 패러다임: Stream-based Dataflow Systems
- 스트림 처리 시스템도 “작은 단위의 연산(operators)”을 연결하여 큰 시스템을 구성한다는 점에서 마이크로서비스와 유사함.
- 그러나 핵심 차이는 통신 방식에 있음:
- Microservice → Request/Response (동기식)
- Dataflow → Message Stream (비동기식)
- 즉, “함수 호출”이 아닌 “데이터 흐름(event flow)”으로 상호작용한다.
- 스트림 기반 접근의 본질: Stream Join
- 이 접근은 “RPC 호출”을 “스트림 조인(stream join)”으로 대체한 것.
- 두 이벤트 스트림:
- purchase events (구매 이벤트)
- exchange rate updates (환율 이벤트)
- 두 스트림을 시간 기준으로 조인하여 → “구매 시점의 환율”을 결합.
- 시간 의존성(time-dependence) 주의:
- 나중에 재처리할 경우 환율이 달라질 수 있음.
- 따라서 “구매 시점의 환율”을 복원하려면 과거 환율 이벤트를 함께 보관해야 함.
- 데이터플로우 접근은 “요청 기반(request-driven)” → “이벤트 기반(event-driven)” 으로의 진화다.
🌀 2-3. 파생 상태 관찰하기

- Write Path는 즉시 처리(eager), Read Path는 요청 시 처리(lazy) 라는 관점에서 서로 보완 관계를 이룬다.
| 설계 방향 | 특징 | 예시 |
| Write-heavy (사전 계산 중심) | - 쓰기 시점에 많은 계산 수행- 읽기 시 매우 빠름 | 검색 인덱스, Materialized View |
| Read-heavy (요청 시 계산 중심) | - 쓰기는 가볍지만- 읽기 시 많은 계산 필요 | OLTP 쿼리, 실시간 집계 |
| 균형형 | - 일부는 사전 계산, 일부는 실시간 계산 | 캐시 + 비동기 업데이트 |
- 파생 데이터(derived dataset)는 “미리 계산할지, 나중에 계산할지” 의 균형점이다.
- 시스템 설계는 “계산 시점”을 어디에 둘 것인가의 문제다.
구체화 뷰와 캐싱
- 인덱스는 “쓰기 시점에 미리 정리해두는 구조"이므로 읽기 속도를 빠르게 만들지만, 쓰기 오버헤드 존재
- 현실적 절충: Common Queries Cache
- 자주 등장하는 쿼리만 미리 계산(cache or materialized view) 해둠.
- 나머지 쿼리는 기존 인덱스를 이용해 실시간 검색.
- 이 구조를 “common query cache” 혹은 “materialized view” 라고 부른다.
오프라인 대응 가능한 상태 저장 클라이언트
➔ “만약 클라이언트가 자체적으로 상태(state)를 갖는다면, 서버-클라이언트 구조는 어떻게 달라질까?”
- 지난 20년간 웹 애플리케이션은 “서버 중심, 클라이언트 무상태" 모델을 기본으로 함.
- PA(Single Page Application)와 모바일 앱의 등장으로 패러다임이 바뀜
- 클라이언트가 저장하는 데이터는 서버의 상태(state) 를 부분 복제(partial replica) 한 것.
- 서버 → 진리의 원본(Source of Truth)
- 클라이언트 → 구체화 뷰 혹은 캐시
- 서버의 상태 변경이 클라이언트에 반영될 때까지 동기화 지연(sync delay) 존재함
상태 변경을 클라이언트에게 푸시하기
➔ 로컬 상태를 어떻게 서버 상태와 동기화할 것인가
- 기존의 웹 패턴
- Polling 기반, Stale Cache
- 변화 (Push 기반 프로토콜의 등장)
- Server-Sent Events (SSE), WebSocket 같은 기술이 등장하면서
- 서버 → 클라이언트 방향의 푸시(push) 통신이 가능해짐.
- 클라의 로컬 상태는 더 이상 정적 캐시가 아니라,
서버 이벤트 스트림과 실시간으로 동기화되는 복제 상태가 된다.
- 기존에는 Write Path가 서버 내부의 파생 데이터(인덱스, 뷰 등) 까지만 도달
- 하지만 Write Path가 클라이언트까지 확장됨.
- 즉, 서버의 변경 이벤트가 클라이언트로 스트림 형태로 전송.
- 오프라인 상태를 고려한 동기화 전략
- 클라이언트는 종종 오프라인 상태가 되므로, 서버의 이벤트를 받을 수 없는 시간 구간이 생김.
- 로그 기반 메시징 시스템(Kafka 등)의 offset 재연결 패턴으로 해결 가능
종단 간 이벤트 스트림
- 현대 데이터 시스템의 미래는 “end-to-end event streams”이다.
- 즉, 사용자의 입력부터 다른 사용자 화면의 변화까지 하나의 이벤트 스트림으로 연결되는 구조.
- 이를 위해서는 “stateless request/response” 패러다임을 넘어
“stateful, publish/subscribe dataflow” 로 전환해야 한다. - 데이터를 질의하는 대신 변화를 구독(subscribe)해야 한다.\
읽기도 이벤트다
- 기존 구조: “읽기와 쓰기”의 분리
- 지금까지의 모델에서는 다음처럼 역할이 나뉨:
- Write Path: 이벤트 로그를 기반으로 파생 데이터 생성.
- Read Path: 저장소(DB, Cache, Index)를 쿼리하여 결과 반환.
- 즉, 읽기(Read)는 한 번의 네트워크 요청, 쓰기(Write)는 이벤트 스트림의 일부로만 다뤄짐.
- 그러나 저자는 읽기도 이벤트로 볼 수 있음.
- 지금까지의 모델에서는 다음처럼 역할이 나뉨:
- 새로운 관점: “읽기 요청도 이벤트다”
- 즉, 읽기 요청(read query)을 하나의 이벤트(read event) 로 표현하고,
- 이를 스트림 프로세서(stream processor) 가 처리할 수 있음.
- 새로운 데이터플로우 구조:
- [Read Request Event Stream] → [Stream Processor] → [Read Result Event Stream]
- 사용자는 “요청” 이벤트를 발행(publish)
- 스트림 프로세서가 “결과” 이벤트를 발행(subscribe & process)
- 이 구조는 요청/응답(request/response) 모델을 pub/sub 패러다임으로 대체함.
- [Read Request Event Stream] → [Stream Processor] → [Read Result Event Stream]
- Stream-Table Join으로서의 “읽기”
- 쓰기와 읽기를 모두 이벤트로 표현하면,
“읽기 요청 스트림”과 “데이터 스트림(또는 테이블)”을 조인(join) 하는 형태로 해석할 수 있음.
- 쓰기와 읽기를 모두 이벤트로 표현하면,
- One-off Read vs Subscription Read
- 일회성 조회(one-off read):
- 단일 요청을 조인에 통과시켜 결과를 반환하고 종료.
- 일반적인 “쿼리 1회 실행” 형태.
- 구독형 조회(subscribe request):
- 지속적으로 조인 상태를 유지.
- 데이터가 바뀌면 새 결과를 푸시.
- 즉, “Reactive Query / Live Query” 모델.
- 일회성 조회(one-off read):
- 읽기 이벤트를 기록(Log)할 때의 부가 가치
- 읽기 이벤트를 로그에 남기면,
데이터 혈통(data provenance) 과 인과관계(causality) 추적이 가능함 - 예시:
- 사용자 A가 상품을 봄 → 재고 “있음” 상태 확인 → 구매 결정
- 그 후 재고가 소진됨
- 나중에 “왜 구매 버튼을 눌렀는가?”를 분석하려면
- 사용자가 “당시 어떤 정보를 봤는지” 기록이 필요함 → 읽기 로그
- 읽기 이벤트를 남기면, 시스템 전체의 원인(Why) 을 재구성할 수 있음.
- 읽기 이벤트를 로그에 남기면,
- 쓰기와 읽기를 모두 로그로 통합하면…
- 읽기 이벤트도 로그에 기록하면:
- 장점: 완전한 인과 추적 가능 (cause & effect)
- 단점: 저장 공간, I/O 부하 증가
- 하지만 이미 쓰기 로그를 운영 중이라면, 읽기 로그도 함께 남기는 것은 자연스러운 확장임.
- 읽기 이벤트도 로그에 기록하면:
다중 파티션 데이터 처리
- 트위터의 분산 RPC
- 다중 파티션에 분산된 데이터를 스트림 조합으로 통합
- 스트림 기반 다중 파티션 쿼리의 장점
- MPP (Massively Parallel Processing) 데이터베이스도 유사한 일을 함.
- 쿼리를 DAG(Directed Acyclic Graph)로 분리
- 각 노드가 병렬로 실행
- 마지막에 결과를 결합
- 스트림 프로세서는 이 구조를 이미 기본적으로 내장하고 있음.
- 따라서 동일한 처리를 실시간으로(streaming) 수행할 수 있음.
- MPP (Massively Parallel Processing) 데이터베이스도 유사한 일을 함.
- 실용적 제안
- 만약 단순히 일회성 쿼리라면, MPP DB를 쓰는 게 더 간단할 수 있음.
- 하지만 지속적 스트림 기반 처리가 필요하다면, 스트림 프로세서 모델이 훨씬 더 적합함.
❐ 3. 정확성을 목표로
🌀 3-1.
데이터 시스템만 믿어서는 “완전하게 안전”하지 않다
- 트랜잭션 격리 수준이 높고, 직렬화 가능한 DB를 사용한다고 해도
애플리케이션 레벨의 버그로 인해 데이터 손실/손상은 발생할 수 있다. - 예
- 애플리케이션 버그로 잘못된 값을 UPDATE
- 삭제하면 안 되는 데이터를 DELETE
→ DB의 직렬화 트랜잭션이 이런 문제를 해결해주지 않는다.
- 데이터 안전은 DB가 아니라 애플리케이션도 함께 책임져야 한다.
Exactly-once 처리의 어려움
- 메시지 처리 중 오류가 생기면 보통 2가지 선택이 있다.
- 포기한다 → 데이터 유실
- 재시도한다 → 실제로는 성공했는데 응답이 끊겨서 재시도하면 중복 처리
- 이 중 “재시도했을 때도 결과가 한 번만 처리되는 것”이 바로 Exactly-once semantics.
- 하지만 현실에서는 정말 구현하기 어렵다.
Idempotence(멱등성)으로 해결하는 방법
- 가장 효과적인 방식은 작업 자체를 멱등(idempotent) 하게 만드는 것.
- 멱등성을 지원하려면 다음 같은 추가 메타데이터가 필요할 수 있다.
- 요청 ID
- 실행된 작업 ID 로그
- fencing token(노드 장애 시 중복 실행 방지) 등
Duplicate suppression(중복 억제)가 필요한 이유
- TCP 같은 네트워크 계층도 중복을 억제해주지만, 이는 단일 TCP 연결 안에서만 의미가 있다.
- 문제는:
- 요청이 DB에 전달되고 COMMIT 했는데
- 클라이언트가 응답을 못 받고 타임아웃 → 다시 요청
- 이 경우 DB 트랜잭션 레벨에서 duplicate suppression이 되지 않으면 중복 처리 위험이 있다.
“고급 트랜잭션 프로토콜”도 완벽하지 않다
- 2PC 같은 프로토콜은 TCP 연결과 트랜잭션을 분리해주지만,
여전히 애플리케이션이 중복 요청을 보낼 때까지 막지는 못한다.
애플리케이션 레벨에서 Operation ID 추가하기
- 중복 요청을 확실히 막으려면 애플리케이션에서 end-to-end로 중복을 억제해야 한다.
- 핵심 아이디어
- 클라이언트가 request_id(UUID) 를 생성
- 서버로 POST할 때 함께 보냄
- DB에서 request_id를 PK/UNIQUE로 사용하여 이미 처리된 요청이면 INSERT가 실패 → 중복 방지
End-to-End Argument란 무엇인가?
- Saltzer, Reed, Clark이 정의한 개념
- 어떤 기능이 정말로 필요하다면, 시스템의 끝단(end-to-end) 에서 보장해야 한다.
- 낮은 레벨(TCP, 네트워크, 등)의 보장만으로는 부족하다.
- 예시
- Duplicate suppression
- TCP는 패킷 중복을 해결해줘도 클라이언트 → 서버 → DB 전체에 걸친 중복은 막지 못함.
- 그러므로 duplicate suppression은 DB까지 end-to-end로 관통해야 한다.
- 데이터 무결성 체크
- TCP/Ethernet 체크섬은 전송 중 오류만 잡아낸다
- 하지만 서버 버그, 디스크 손상은 감지 못함
→ 결국 end-to-end 체크가 필요 (예: 애플리케이션 레벨 체크섬)
- 암호화
- WiFi 암호화는 집 안 공격자만 막음
- 서버 공격자는 못 막음 → TLS처럼 end-to-end 암호화가 필요
- Duplicate suppression
결론 — 데이터 안전은 결국 애플리케이션 책임도 크다
- DB가 강력한 트랜잭션을 제공해도 중복 억제, 멱등성, end-to-end 검증 없이는 데이터 손상 가능
- 트랜잭션은 많은 문제를 “commit or abort”로 추상화하지만 현실의 장애는 더 복잡하다
- 대규모 분산 환경에서는 애플리케이션 수준의 end-to-end 안전성이 필수
🌀 3-2. 제약 조건 강제하기
유니크 제약(uniqueness constraint)의 어려움
- 유저네임, 이메일, 계좌 ID처럼 “하나만 있어야 하는 값”을 보장하는 것은 쉬워 보이지만,
분산된 환경에서는 매우 어렵다. - 예를 들어:
- 두 개의 노드가 동시에 같은 이메일로 회원가입
- 두 데이터센터에서 같은 좌석을 동시에 예약
→ 모두 “유일해야 하는 값”이기 때문에 충돌을 해결해야 한다.
왜 어려울까? → 유니크 보장은 합의(consensus)를 필요로 하기 때문
- 여러 노드가 동시에 같은 값을 삽입하려고 할 때,
시스템은 어느 요청이 승자이며, 나머지는 거절해야 하는지 결정해야 한다. - 이는 결국 합의(consensus) 문제다.
- 가장 간단한 방식:
- 리더(leader)를 하나 두고, 모든 유니크 판단을 리더가 하게 한다.
- 문제점:
- 리더 노드 장애 → 다시 합의 필요
- 리더 한 개가 처리량 병목이 됨
- 클라이언트가 지구 반대편 → 높은 지연(latency)
파티셔닝 기반 유니크 보장
- 유니크 조건을 값 기반으로 파티션하면 확장성이 높아진다.\
- 예
- 요청 ID를 key로 파티션 → 같은 request_id는 항상 같은 파티션으로 감
- username을 hash(username) 기준으로 파티션 → 동일 username 충돌은 같은 파티션에서만 처리
- 즉, 동일 값을 유니크하게 만들려면 같은 파티션에서 처리되도록 라우팅하면 된다.
- 이 방식의 장점
- 파티션 별로 독립적 처리 가능 → 확장성 높음
- 각 파티션에서 순서가 보장되면(total order), 충돌 해결 가능
- 하지만 단점도 있다
- 비동기 멀티마스터 복제에서는 불가능(서로 다른 노드가 서로 모르게 같은 값을 허용할 수 있기 때문)\
- 유니크를 강하게 보장하려면 동기적 합의 또는 파티션 기반의 순차 처리가 필요함.
로그 기반 메시징에서 유니크 보장하기
- Kafka처럼 로그 기반 메시징 시스템을 사용하면 유니크 제약을 비교적 쉽게 강하게 보장할 수 있다.
- 핵심 이유:
- 로그는 모든 메시지를 순서대로(total order) 기록
- 파티션 단위로는 한 스레드가 순서대로 처리
- 따라서 충돌 상황에서도 “누가 먼저 왔는지” 명확히 판단 가능
Multi-partition Request Processing (여러 파티션이 관여하면?)
- 문제는 여기서부터다. 금전 이체 같은 작업은 여러 파티션을 동시에 건드린다.
- 전통적인 DB 방식:
- 3개 파티션이 모두 참여 → 분산 트랜잭션(2PC) 필요
- 처리량 저하 + 높은 지연 → 문제가 많음
분산 트랜잭션 없이도 “동일한 정확성”을 달성하는 법
- 2단계 파이프라인 분리 + 로그 기반 처리 + 멱등성 조합이다.
- 단계 1 — 요청 ID를 기반으로 단일 메시지로 로깅
- 클라이언트가 송금 요청을 하나의 메시지로 보내고
- 로그에 append (request_id 파티션)
- 단계 2 — stream processor가 이 메시지를 읽고, 두 개의 별도 명령을 생성
- A 계좌 파티션: “A 계좌에서 10 빼세요”
- B 계좌 파티션: “B 계좌에 10 더하세요”
- 단계 3 — 두 계좌 파티션 소비자가 각각 명령 적용
- request_id를 기준으로 중복 제거
- 명령이 여러 번 와도 멱등 적용 가능
- 결과:
- A에서 빠지고 B에서 더하는 작업 모두 exactly once 적용됨
- 단 분산 트랜잭션 없이도 동일한 정확성 확보
장애 상황에서도 안전한 이유
➔ 만약 단계 2의 프로세서가 크래시 한다면?
- 체크포인트에서 재시작
- 로그에서 다시 읽고 같은 debit/credit 메시지 생성
- 하지만 단계 3에서 request_id로 dedupe → 중복 방지
- 즉,
- 로그는 append-only
- 처리 과정은 deterministic
- 결과는 멱등 → “한 번만 처리된 것처럼” 보장 가능
🌀 3-3. 적시성과 무결성
Transactions는 원래 “Timely”하다 (즉, Linearizable)
- ACID 트랜잭션은 다음을 보장한다.
- Commit이 끝난 직후, 모든 읽기는 그 결과를 본다.
- 즉, 쓰기(Write) → Commit → Read 가 즉시 반영된다.
- 이게 바로 선형성
하지만 Stream Processing에서는 이게 깨진다
- Kafka처럼 로그 기반 비동기 파이프라인에서:
- 클라이언트는 메시지를 전송만 하고
- 실제 처리는 비동기 파이프라인에서 나중에 일어난다
- 그래서 “commit하자마자 읽으면 반영된다”는 보장이 없다
- 즉, 타임라인 상 지연이 생기는 게 기본이다.
Timeliness = “업데이트된 최신 상태를 얼마나 빨리 볼 수 있나?”
- 복제지연(replication lag) 때문에
내가 조금 전 업데이트한 데이터를 다른 노드에서는 잠시동안 못볼 수 있다. - 하지만 몇 초 안에 eventually 반영됨.
- Timeliness는 결국 “언젠가는 최신 상태가 되게 하는 속성”.
CAP의 “Consistency”도 사실 Timeliness 의미에 가깝다
- Linearizability를 제공하는 시스템 = “강한 최신성 보장 시스템”.
- 그리고 더 약한 형태도 있다:
- Read-after-write consistency
- Monotonic reads 등
Integrity = “데이터 자체가 틀리지 않음”
- Integrity는 timeliness보다 훨씬 더 중요하며 핵심적이다.
- Integrity는 이런 것을 의미한다:
- 데이터가 손상되지 않았다 (no corruption)
- 데이터가 사라지지 않았다 (no data loss)
- 모순된 상태가 없다 (no contradictory state)
- 파생 데이터는 원본 데이터와 정확히 일치해야 한다
Integrity가 깨지면 “복구가 불가능하다”
- Timeliness 문제는 시간이 지나면 해결되지만,
- Integrity 문제는:
- 기다린다고 해결되지 않는다
- 복구 작업이 필요하다 (manual repair)
- 장애가 치명적이다
4. Event-driven 시스템은 Timeliness와 Integrity를 “분리”한다
- ACID 세계에서는 동시에 제공되지만 이벤트 기반 스트림 시스템에서는 재밌는 특징이 있다:
- Integrity를 보장하면서, Timeliness를 포기할 수 있다.
- 즉,
- 파이프라인은 비동기일 수 있고
- 최신 데이터가 바로 반영되지 않아도
- Integrity만 보장되면 된다
- Integrity를 지키는 핵심 기법들
- Exactly-once / Effectively-once 처리
- 멱등(idempotent) 연산
- 중복 제거(deduplication)
- 요청 ID(request_id)를 end-to-end로 전달
- 불변 로그(immutable log)에 기반한 재처리
- 이 조합 덕분에 스트림 시스템은 오류가 나도 정합성을 유지할 수 있다.
5. 스트림 시스템에서 Integrity를 보장하는 패턴
- 모든 write를 단일 메시지로 표현 → 원자적으로 기록
- 모든 파생 상태는 deterministic function으로 유도
- request_id를 end-to-end로 전달 → 중복 억제
- 모든 메시지를 immutable하게 관리
6. Loosely interpreted constraints (느슨한 제약 조건)
- 현실에서는 “유니크 제약”이 흔히 느슨하게 해도 된다
- 엄격하게 리니어라이저블하게 처리할 필요가 없는 상황 예:
- 동시에 두 사람이 같은 좌석을 예약 → 한쪽에게 사과하고 다른 좌석 제안
- 재고 5개인데 6개 주문됨 → 사과하고 환불/지연 안내
- 호텔 오버부킹(일반적 패턴) → 보상 제공
- 계정 overdraft → 나중에 수수료 부과
- 즉,
- 완벽한 정확성은 비싸다
- 실제 비즈니스는 사과/보상 프로세스로 수습 가능하다
- Integrity는 유지하되 Timeliness는 굳이 엄격할 필요가 없다
7. Coordination-avoiding Data Systems (조율을 피하는 시스템)
- 두 가지 관찰:
- 스트림 시스템은 분산 트랜잭션 없이도 integrity 보장 가능
- 많은 제약 조건은 임시 위반 가능(나중에 사과해서 해결할 수 있다)
- 즉, 동기적 조율(synchronous coordination) 없이도 정확성을 확보할 수 있는 시스템을 만들 수 있다.
- 협업이 필요 없는 대신:
- 로그 기반 비동기 처리
- request_id
- 멱등 처리
- 재생 가능한 파이프라인
- 느슨한 timeliness
- 이런 시스템의 장점
- 더 높은 가용성
- 더 높은 성능
- 지연이 적음
- 장애 복구에 강함
- 조율 비용이 거의 없음
- AWS Dynamo, Cassandra, Kafka 기반 시스템 등이 이 철학을 따른다.
🌀 3-3. 믿어라 하지만 확인하라.
시스템은 항상 “잘못될 수 있다”
- 이런 일들은 드물지만 “언젠가는 반드시” 발생한다.
- 하드웨어 비트 플립
- 디스크 silent corruption
- 네트워크 데이터 손상
- DB 버그
- 애플리케이션 버그
가장 위험한 태도 = 기술을 맹신하는 것
- “트랜잭션이니까 데이터는 틀리지 않겠지”
- “DB가 알아서 무결성 지켜주겠지”
해결책 = Auditing & Verification
- 데이터가 손상됐는지 정기적으로 확인
- 백업 복원 테스트
- 해시 기반 데이터 검증
- 이벤트 로그 기반 재처리
- end-to-end 검증
- self-validating/self-auditing 시스템 구축
Event sourcing과 로그 기반 시스템은 Auditing에 매우 적합
- event는 불변
- deterministic derivation → 재현 가능
- provenance(데이터의 기원) 추적 용이
- hash 기반 무결성 검증 가능
앞으로의 데이터 시스템은 “Trust, but verify” 철학을 따른다
- 암호학적 무결성 검증(Merkle Tree 등)
- 자체 감사 기능(self-audit)
- 지속적 end-to-end 검증
'Book > 데이터 중심 애플리케이션 설계' 카테고리의 다른 글
| 11. 스트림 처리 (0) | 2025.11.01 |
|---|---|
| 10장. 일괄 처리(Batch Processing) (0) | 2025.10.26 |
| Part3. 파생 데이터 (Derived Data) (0) | 2025.10.26 |
| 9장. 일관성과 합의 (Consistency and Consensus) (0) | 2025.10.17 |
| 8장. 분산 시스템의 골칫거리(The Trouble with Distributed Systems) (0) | 2025.10.13 |