Eventually consistent(최종적 일관성)

Psj·2026년 4월 20일

sql

목록 보기
12/12

Eventually consistent
= 결과적 일관성
= 최종적 일관성

"지금 당장은 안맞을 수 있지만
결국엔 (eventually) 일관된 상태로 맞춰진다"

문제 상황 — 동시에 다른 데이터가 쓰여지면

Cassandra 서버 3대 (복제본 3개)

[노드1] [노드2] [노드3]

사용자 A가 노드1에 "좋아요 수 = 100" 씀
사용자 B가 동시에 노드2에 "좋아요 수 = 101" 씀

지금 상태
  노드1: 100
  노드2: 101
  노드3: 아직 동기화 안됨 (옛날 값 99)

→ 세 노드가 다 다른 값
→ 이게 Soft State (잠깐 안맞는 상태)

이걸 어떻게 맞추는지가 핵심이에요.


해결 방법 1 — Last Write Wins (LWW)

가장 많이 쓰는 방식이에요. 타임스탬프가 최신인 게 이김.

사용자 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의 오래된 데이터가 이겨버림
→ 데이터 유실

이걸 해결하려고 나온 게 다음 방법이에요.


해결 방법 2 — Vector Clock (벡터 시계)

시계 대신 버전 번호로 인과관계를 추적해요.

각 노드가 자기 버전 카운터를 가짐

초기 상태
  노드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 (쿼럼)

쓸 때 여러 노드에 동시에 써서 불일치 자체를 줄여요.

노드 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 → 빠르지만 옛날값 읽힐 수 있음

해결 방법 4 — Read Repair + Anti-Entropy

동기화를 능동적으로 맞추는 방법이에요.

Read Repair (읽을 때 고침)

클라이언트가 읽기 요청
  → 여러 노드에서 동시에 읽음
  → 값이 다른 노드 발견
  → 최신 값으로 뒤처진 노드 업데이트
  → 클라이언트에 최신값 반환

자연스럽게 읽기 요청이 많을수록 빠르게 동기화됨
Anti-Entropy (주기적 동기화)

백그라운드에서 노드끼리 계속 비교

[노드1] ←── 주기적 비교 ──→ [노드2]
  "나 100인데 너는?"
  "나 101이야"
  "그럼 나도 101로 업데이트"

Merkle Tree를 써서 효율적으로 비교
  전체 데이터 비교 대신
  해시값만 비교해서 다른 부분만 찾아서 동기화

실제로 Eventually Consistent가 얼마나 걸리나

같은 데이터센터 (같은 지역)
  수십 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는 마법이 아니라 이 다섯 가지 메커니즘이 조합된 엔지니어링이에요. 그래서 정합성이 덜 중요한 데이터에만 쓰는 거예요. 잠깐 틀려도 괜찮은 데이터에서만 이 방식이 의미있어요.

profile
Software Developer

0개의 댓글