Eventually consistent
= 결과적 일관성
= 최종적 일관성"지금 당장은 안맞을 수 있지만
결국엔 (eventually) 일관된 상태로 맞춰진다"
Cassandra 서버 3대 (복제본 3개)
[노드1] [노드2] [노드3]
사용자 A가 노드1에 "좋아요 수 = 100" 씀
사용자 B가 동시에 노드2에 "좋아요 수 = 101" 씀
지금 상태
노드1: 100
노드2: 101
노드3: 아직 동기화 안됨 (옛날 값 99)
→ 세 노드가 다 다른 값
→ 이게 Soft State (잠깐 안맞는 상태)
이걸 어떻게 맞추는지가 핵심이에요.
가장 많이 쓰는 방식이에요. 타임스탬프가 최신인 게 이김.
사용자 A: 좋아요 100 (timestamp: 09:00:00.100)
사용자 B: 좋아요 101 (timestamp: 09:00:00.150)
노드들이 동기화할 때
"타임스탬프 비교해서 최신 것만 살림"
노드1: 100 (09:00:00.100)
노드2: 101 (09:00:00.150) ← 이게 이김
노드3: 101로 덮어씌워짐
최종 상태
노드1: 101
노드2: 101
노드3: 101
→ Eventually Consistent 달성
단, 문제가 있어요.
LWW의 함정 — 시계가 틀리면?
분산 시스템에서 각 서버의 시계가 정확히 일치하지 않음
노드1 시계: 09:00:00.100
노드2 시계: 09:00:00.050 ← 시계가 100ms 느림
사용자 B가 노드2에 나중에 썼는데
타임스탬프는 오히려 더 작음
→ 사용자 A의 오래된 데이터가 이겨버림
→ 데이터 유실
이걸 해결하려고 나온 게 다음 방법이에요.
시계 대신 버전 번호로 인과관계를 추적해요.
각 노드가 자기 버전 카운터를 가짐
초기 상태
노드1: { 값: 99, clock: [노드1:0, 노드2:0, 노드3:0] }
노드2: { 값: 99, clock: [노드1:0, 노드2:0, 노드3:0] }
노드3: { 값: 99, clock: [노드1:0, 노드2:0, 노드3:0] }
사용자 A가 노드1에 100 씀
노드1: { 값: 100, clock: [노드1:1, 노드2:0, 노드3:0] }
사용자 B가 동시에 노드2에 101 씀
노드2: { 값: 101, clock: [노드1:0, 노드2:1, 노드3:0] }
동기화할 때
노드1: [1, 0, 0]
노드2: [0, 1, 0]
→ 어느 쪽이 최신인지 알 수 없음
→ "충돌(Conflict) 감지!"
→ 두 값이 동시에 발생한 독립적인 변경임을 알게 됨
충돌 해결은 두 가지 방식이에요.
방법 A — 자동 해결 (LWW로 fallback)
충돌 감지 → 그냥 최신 타임스탬프 것 선택
→ 단순하지만 데이터 유실 가능
방법 B — 애플리케이션이 해결
충돌 감지 → 둘 다 저장 → 애플리케이션에 충돌 알림
→ 애플리케이션이 비즈니스 로직으로 해결
좋아요 수 충돌 예시
노드1: 100
노드2: 101
→ 둘 중 더 큰 값 선택 (좋아요는 줄어들면 안되니까)
→ 101 선택
아마존 Dynamo가 이 방식 사용해요.
쓸 때 여러 노드에 동시에 써서 불일치 자체를 줄여요.
노드 3대, Quorum 설정: W=2, R=2
W=2: 쓸 때 최소 2개 노드에 성공해야 완료
R=2: 읽을 때 최소 2개 노드에서 읽어서 비교
쓰기
사용자 A: 좋아요 100
→ 노드1에 씀 ✅
→ 노드2에 씀 ✅
→ 노드3은 아직 (백그라운드)
→ W=2 충족 → 성공 반환
사용자 B: 좋아요 101 (동시에)
→ 노드2에 씀 ✅
→ 노드3에 씀 ✅
→ 노드1은 아직 (백그라운드)
→ W=2 충족 → 성공 반환
현재 상태
노드1: 100
노드2: 101 (A, B 둘 다 씀 → 최신: 101)
노드3: 101
읽기 (R=2)
노드1, 노드2에서 읽음
노드1: 100, 노드2: 101
→ 다름 → 최신 타임스탬프인 101 반환
→ 노드1도 101로 업데이트 (Read Repair)
핵심 공식
W + R > N 이면 강한 일관성 보장
N=3 (노드 수)
W=2, R=2 → W+R=4 > N=3 → 항상 최신값 읽힘
N=3
W=1, R=1 → W+R=2 < N=3 → 빠르지만 옛날값 읽힐 수 있음
동기화를 능동적으로 맞추는 방법이에요.
Read Repair (읽을 때 고침)
클라이언트가 읽기 요청
→ 여러 노드에서 동시에 읽음
→ 값이 다른 노드 발견
→ 최신 값으로 뒤처진 노드 업데이트
→ 클라이언트에 최신값 반환
자연스럽게 읽기 요청이 많을수록 빠르게 동기화됨
Anti-Entropy (주기적 동기화)
백그라운드에서 노드끼리 계속 비교
[노드1] ←── 주기적 비교 ──→ [노드2]
"나 100인데 너는?"
"나 101이야"
"그럼 나도 101로 업데이트"
Merkle Tree를 써서 효율적으로 비교
전체 데이터 비교 대신
해시값만 비교해서 다른 부분만 찾아서 동기화
같은 데이터센터 (같은 지역)
수십 ms ~ 수백 ms 내에 동기화
다른 데이터센터 (다른 지역)
수초 내에 동기화
실제 체감
카카오톡 메시지 순서가 가끔 바뀌는 것
→ 이게 Eventually Consistent의 흔적
→ 짧은 시간 후 올바른 순서로 수렴
좋아요 수
잠깐 100이었다가 101이 됨
→ 사용자가 못 느낌 → 괜찮음
채팅 메시지
순서가 1~2개 잠깐 바뀔 수 있음
→ 금방 올바른 순서로 수렴 → 괜찮음
조회수
동시에 여러 서버에 쓰여서 잠깐 다를 수 있음
→ 결국 합산됨 → 괜찮음
송금
A 계좌에서 빠지고 B 계좌에 안 들어오는 순간이
단 1ms라도 존재하면 안됨
→ Eventually Consistent 절대 불가
→ MySQL 트랜잭션 필수
Eventually Consistent 동작 원리
1. LWW: 타임스탬프 최신것 선택
2. Vector Clock: 버전으로 인과관계 추적 → 충돌 감지
3. Quorum: 쓸 때 여러 노드 동시 기록 → 불일치 최소화
4. Read Repair: 읽을 때 뒤처진 노드 자동 업데이트
5. Anti-Entropy: 백그라운드 주기적 동기화
이 다섯 가지가 조합되어
"잠깐 틀리다가 결국 맞춰짐"을 구현
결국 Eventually Consistent는 마법이 아니라 이 다섯 가지 메커니즘이 조합된 엔지니어링이에요. 그래서 정합성이 덜 중요한 데이터에만 쓰는 거예요. 잠깐 틀려도 괜찮은 데이터에서만 이 방식이 의미있어요.